Преглед изворни кода

Merge branch 'master' into feature/operation-record

Zimo пре 1 дан
родитељ
комит
22433792eb
79 измењених фајлова са 15671 додато и 7337 уклоњено
  1. 1 1
      .env.local
  2. 49 0
      src/api/digital/index.ts
  3. 10 0
      src/api/pms/fiveconstructiondailyreport/index.ts
  4. 65 0
      src/api/pms/qhse/index.ts
  5. 6 0
      src/api/pms/stat/index.ts
  6. 2 0
      src/components/ZmTable/index.vue
  7. 4 1
      src/utils/dict.ts
  8. 731 0
      src/views/digital/index.vue
  9. 58 0
      src/views/iot/commonThing/ThingModelEvent.vue
  10. 14 23
      src/views/iot/commonThing/ThingModelForm.vue
  11. 151 0
      src/views/iot/commonThing/ThingModelInputOutputParam.vue
  12. 177 0
      src/views/iot/commonThing/ThingModelProperty.vue
  13. 64 0
      src/views/iot/commonThing/ThingModelService.vue
  14. 49 0
      src/views/iot/commonThing/ThingModelTSL.vue
  15. 74 0
      src/views/iot/commonThing/components/DataDefinition.vue
  16. 3 0
      src/views/iot/commonThing/components/index.ts
  17. 56 0
      src/views/iot/commonThing/dataSpecs/ThingModelArrayDataSpecs.vue
  18. 160 0
      src/views/iot/commonThing/dataSpecs/ThingModelEnumDataSpecs.vue
  19. 139 0
      src/views/iot/commonThing/dataSpecs/ThingModelNumberDataSpecs.vue
  20. 167 0
      src/views/iot/commonThing/dataSpecs/ThingModelStructDataSpecs.vue
  21. 11 0
      src/views/iot/commonThing/dataSpecs/index.ts
  22. 94 121
      src/views/iot/commonThing/index.vue
  23. 20 15
      src/views/iot/thingmodel/ThingModelForm.vue
  24. 20 3
      src/views/pms/constructionDailyReport/components/ConstructionDailyReportFormSection.vue
  25. 40 14
      src/views/pms/constructionDailyReport/components/ConstructionDailyReportTable.vue
  26. 169 21
      src/views/pms/constructionDailyReport/components/report-config.ts
  27. 53 6
      src/views/pms/constructionDailyReport/index.vue
  28. 307 168
      src/views/pms/device/CustomerList.vue
  29. 409 173
      src/views/pms/failure/IotFailureReportForm.vue
  30. 398 289
      src/views/pms/failure/index.vue
  31. 493 0
      src/views/pms/fiveDailyReportSummary/index.vue
  32. 189 45
      src/views/pms/information/IotInformationDbForm.vue
  33. 220 113
      src/views/pms/information/index.vue
  34. 206 69
      src/views/pms/inspect/item/IotInspectItemForm.vue
  35. 281 148
      src/views/pms/inspect/item/index.vue
  36. 348 176
      src/views/pms/inspect/order/InspectOrderDetail.vue
  37. 938 153
      src/views/pms/inspect/order/WriteOrder.vue
  38. 312 250
      src/views/pms/inspect/order/index.vue
  39. 349 217
      src/views/pms/inspect/plan/InspectRouteList.vue
  40. 388 243
      src/views/pms/inspect/plan/IotInspectPlan.vue
  41. 276 226
      src/views/pms/inspect/plan/index.vue
  42. 306 169
      src/views/pms/inspect/route/InspectItemList.vue
  43. 410 139
      src/views/pms/inspect/route/IotInspectRoute.vue
  44. 267 144
      src/views/pms/inspect/route/index.vue
  45. 308 426
      src/views/pms/iotmainworkorder/DeviceAlarmBomList.vue
  46. 17 25
      src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue
  47. 250 115
      src/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue
  48. 515 257
      src/views/pms/iotprojectinfo/IotProjectInfoForm.vue
  49. 38 54
      src/views/pms/iotprojectinfo/index.vue
  50. 776 707
      src/views/pms/iotprojecttask/IotProjectTaskForm.vue
  51. 249 68
      src/views/pms/iotprojecttask/index.vue
  52. 512 440
      src/views/pms/maintain/IotMaintainAddEdit.vue
  53. 417 222
      src/views/pms/maintain/IotMaintainDetail.vue
  54. 417 188
      src/views/pms/maintain/IotMaintainRecord.vue
  55. 430 328
      src/views/pms/maintain/index.vue
  56. 43 22
      src/views/pms/maintain/material/MaintainMaterialDrawer.vue
  57. 8 1
      src/views/pms/qhse/MeasureCertDrawer.vue
  58. 36 14
      src/views/pms/qhse/certificate.vue
  59. 15 10
      src/views/pms/qhse/credential/index.vue
  60. 177 0
      src/views/pms/qhse/data/IotInfoFormTree.vue
  61. 0 129
      src/views/pms/qhse/data/IotTreeForm.vue
  62. 413 289
      src/views/pms/qhse/data/PmsTree.vue
  63. 334 362
      src/views/pms/qhse/data/index.vue
  64. 11 13
      src/views/pms/qhse/deviceCert/index.vue
  65. 6 8
      src/views/pms/qhse/emergencyDrill/bookCheck/index.vue
  66. 6 8
      src/views/pms/qhse/emergencyDrill/index.vue
  67. 11 12
      src/views/pms/qhse/hazard/index.vue
  68. 91 13
      src/views/pms/qhse/index.vue
  69. 26 2
      src/views/pms/qhse/iotmeasuredetect/IotMeasureDetectForm.vue
  70. 8 1
      src/views/pms/qhse/iotmeasuredetect/index.vue
  71. 29 27
      src/views/pms/qhse/jsa/index.vue
  72. 632 18
      src/views/pms/qhse/kanban/index.vue
  73. 21 13
      src/views/pms/qhse/ptw/index.vue
  74. 44 10
      src/views/pms/qhse/safety/index.vue
  75. 661 313
      src/views/pms/stat/inspect.vue
  76. 540 287
      src/views/pms/stat/maintain.vue
  77. 71 11
      src/views/pms/stat/rdkb/rd-rate.vue
  78. 71 11
      src/views/pms/stat/rhkb/equipment-rate.vue
  79. 4 6
      src/views/pms/stat/rhkb/rhsummary.vue

+ 1 - 1
.env.local

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

+ 49 - 0
src/api/digital/index.ts

@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface DigitalWell {
+  id: number
+  well?: string
+  uwi?: string
+  comp?: string
+  fld?: string
+  cnty?: string
+  stat?: string
+  start?: number
+  stop?: number
+  step?: number
+  depth?: number | null
+  elev?: number
+  lat?: number
+  lon?: number
+  [key: string]: unknown
+}
+
+export interface WellListResult {
+  list: DigitalWell[]
+  total: number
+}
+
+export interface WellLogDataset {
+  wellInfo?: DigitalWell
+  depths: number[]
+  sws?: number[]
+  dts?: number[]
+  porns?: number[]
+  rhobs?: number[]
+  rxos?: number[]
+  r25s?: number[]
+  cilds?: number[]
+  calis?: number[]
+  sps?: number[]
+  grs?: number[]
+  [key: string]: unknown
+}
+
+export const DigitalApi = {
+  getWellLogDataset: (wellId: number): Promise<WellLogDataset> => {
+    return request.get({ url: '/pms/las-well-data/wellLogDataset', params: { wellId } })
+  },
+  getWellList: (): Promise<WellListResult> => {
+    return request.get({ url: '/pms/las-well/page', params: { pageNo: 1, pageSize: 100 } })
+  }
+}

+ 10 - 0
src/api/pms/fiveconstructiondailyreport/index.ts

@@ -12,5 +12,15 @@ export const FiveIotProjectDailyReportApi = {
   // 填报项目日报
   updateIotProjectDailyReport: async (data) => {
     return await request.put({ url: '/pms/iot-five-daily-report/update', data })
+  },
+
+  // 日报汇总统计卡片
+  getIotProjectDailyReportSummary: async (params) => {
+    return await request.get({ url: '/pms/iot-five-daily-report/totalWorkload', params })
+  },
+
+  // 获取项目日报分页
+  getProjectDailyReportSummaryPage: async (params) => {
+    return await request.get({ url: '/pms/iot-five-daily-report/summaryStatistics', params })
   }
 }

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

@@ -663,3 +663,68 @@ export const EmergencyDrillCertApi = {
     return await request.download({ url: `/rq/qhse-emergency-cert/export-excel`, params })
   }
 }
+
+// 资料库
+export const DataLibraryApi = {
+  // 查询资料分页
+  getIotInfoPage: async (params: any) => {
+    return await request.get({ url: `/rq/qhse-info/page`, params })
+  },
+  // 查询资料库的子目录及文件
+  getChildContentFile: async (params: any) => {
+    return await request.post({ url: `/rq/qhse-info/child/content-file`, params })
+  },
+  getAllChildContentFile: async (params: any) => {
+    return await request.post({ url: `/rq/qhse-info/all/content-file`, params })
+  },
+  getIotInfoFilePage: async (params: any) => {
+    return await request.post({ url: `/rq/qhse-info/file/page`, params })
+  },
+  // 查询资料详情
+  getIotInfo: async (id: number) => {
+    return await request.get({ url: `/rq/qhse-info/get?id=` + id })
+  },
+
+  // 新增资料
+  createIotInfo: async (data) => {
+    return await request.post({ url: `/rq/qhse-info/create`, data })
+  },
+
+  // 修改资料
+  updateIotInfo: async (data) => {
+    return await request.put({ url: `/rq/qhse-info/update`, data })
+  },
+
+  // 删除资料
+  deleteIotInfo: async (id: number) => {
+    return await request.delete({ url: `/rq/qhse-info/delete?id=` + id })
+  },
+
+  // 导出资料 Excel
+  exportIotInfo: async (params) => {
+    return await request.download({ url: `/rq/qhse-info/export-excel`, params })
+  },
+
+  // 获得资料库目录分页
+  getFileTreeList: async () => {
+    return await request.get({ url: `/rq/qhse-file-tree/simple-list` })
+  },
+
+  // 新增目录
+  createFileTree: async (data) => {
+    return await request.post({ url: `/rq/qhse-file-tree/create`, data })
+  },
+
+  // 修改目录
+  updateFileTree: async (data) => {
+    return await request.put({ url: `/rq/qhse-file-tree/update`, data })
+  },
+  // 删除目录
+  deleteFileTree: async (id: number) => {
+    return await request.delete({ url: `/rq/qhse-file-tree/delete?id=` + id })
+  },
+
+  getParentIds: async (params: any) => {
+    return await request.get({ url: `/rq/qhse-file-tree/ids/` + params })
+  }
+}

+ 6 - 0
src/api/pms/stat/index.ts

@@ -171,9 +171,15 @@ export const IotStatApi = {
   getRhSevenDayUtilization: async () => {
     return await request.get({ url: `/rq/stat/rh/device/sevenDayUtilization` })
   },
+  getRhSixMonthUtilization: async () => {
+    return await request.get({ url: `/rq/stat/rh/device/sixMonthUtilization` })
+  },
   getRdSevenDayUtilization: async () => {
     return await request.get({ url: `/rq/stat/rd/device/sevenDayUtilization` })
   },
+  getRdSixMonthUtilization: async () => {
+    return await request.get({ url: `/rq/stat/rd/device/sixMonthUtilization` })
+  },
   getRyRate: async (params: any) => {
     return await request.get({ url: `/rq/stat/ry/device/utilizationRate`, params })
   },

+ 2 - 0
src/components/ZmTable/index.vue

@@ -725,6 +725,8 @@ defineExpose({
   }
 
   .el-table__empty-block {
+    width: 100% !important;
+    min-width: 100%;
     min-height: var(--zm-table-empty-min-height);
     background: var(--zm-table-empty-bg);
   }

+ 4 - 1
src/utils/dict.ts

@@ -320,6 +320,7 @@ export enum DICT_TYPE {
 
   // QHSE
   MEASURE_TYPE = 'measure_type',
+  MEASURE_STATUS = 'measure_status',
   PERSON_CERT = 'person_cert',
   ORG_CERT = 'org_cert',
   DANGER_GRADE = 'danger_grade',
@@ -328,7 +329,9 @@ export enum DICT_TYPE {
   QHSE_PTW_TYPE = 'qhse_ptw_type',
   QHSE_PTW_GRADE = 'qhse_ptw_grade',
   QHSE_HAZARD_SOURCE = 'hazard_soure',
-  QHSE_DEVICE_CERT_TYPE = 'qhse_device_cert_type'
+  QHSE_DEVICE_CERT_TYPE = 'qhse_device_cert_type',
+  QHSE_DETECT_CYCLE = 'detect_cycle',
+  QHSE_TRAIN_CYCLE = 'qhse_train_cycle'
 }
 
 export function realValue(type: any, value: string) {

+ 731 - 0
src/views/digital/index.vue

@@ -0,0 +1,731 @@
+<script lang="ts" setup>
+import { DigitalApi, type DigitalWell, type WellLogDataset } from '@/api/digital'
+import { Search } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+
+defineOptions({ name: 'DigitalWellLog' })
+
+type CurveKey =
+  | 'sws'
+  | 'dts'
+  | 'porns'
+  | 'rhobs'
+  | 'rxos'
+  | 'r25s'
+  | 'cilds'
+  | 'calis'
+  | 'sps'
+  | 'grs'
+
+interface CurveConfig {
+  name: string
+  key: CurveKey
+  unit: string
+  min: number
+  max: number
+  color: string
+  track: number
+  inverse?: boolean
+  lineType?: 'solid' | 'dashed' | 'dotted'
+}
+
+const curves: CurveConfig[] = [
+  { name: 'GR', key: 'grs', unit: 'API', min: 30, max: 300, color: '#16a34a', track: 2 },
+  { name: 'SP', key: 'sps', unit: 'mV', min: 10, max: 300, color: '#2563eb', track: 2 },
+  {
+    name: 'CALI',
+    key: 'calis',
+    unit: 'mm',
+    min: 450,
+    max: 800,
+    color: '#dc2626',
+    track: 3,
+    lineType: 'dashed'
+  },
+  {
+    name: 'RESX',
+    key: 'rxos',
+    unit: 'ohm.m',
+    min: 1,
+    max: 30,
+    color: '#f97316',
+    track: 0,
+    lineType: 'dotted'
+  },
+  { name: 'RESS', key: 'r25s', unit: 'ohm.m', min: 1, max: 30, color: '#d946ef', track: 0 },
+  { name: 'RESD', key: 'cilds', unit: 'ohm·m', min: 1, max: 900, color: '#2563eb', track: 3 },
+  { name: 'RHOB', key: 'rhobs', unit: 'g/cm³', min: 1.95, max: 2.95, color: '#111827', track: 0 },
+  {
+    name: 'NPHI',
+    key: 'porns',
+    unit: 'v/v',
+    min: 0,
+    max: 1,
+    color: '#2563eb',
+    track: 0
+  },
+  {
+    name: 'DTC_SINT',
+    key: 'dts',
+    unit: 'us/m',
+    min: 30,
+    max: 200,
+    color: '#7c3aed',
+    track: 1,
+    lineType: 'dashed'
+  },
+  { name: 'SW', key: 'sws', unit: 'v/v_decimal', min: 20, max: 100, color: '#0891b2', track: 1 }
+]
+
+const trackNames = [
+  '低量程(约 0~30)',
+  '中低量程(约 20~200)',
+  '中量程(约 10~300)',
+  '高量程(约 450~900)'
+]
+const trackCurves = trackNames.map((_, track) => curves.filter((curve) => curve.track === track))
+const trackLayout = [
+  { left: '7%', width: '21%' },
+  { left: '29%', width: '21%' },
+  { left: '51%', width: '21%' },
+  { left: '73%', width: '21%' }
+]
+const SAMPLE_LIMIT = 2000
+
+const message = useMessage()
+const chartRef = ref<HTMLElement>()
+const wells = ref<DigitalWell[]>([])
+const selectedWellId = ref<number>()
+const keyword = ref('')
+const dataset = shallowRef<WellLogDataset>()
+const listLoading = ref(false)
+const dataLoading = ref(false)
+const sampledIndices = shallowRef<number[]>([])
+const chart = shallowRef<echarts.ECharts>()
+let resizeObserver: ResizeObserver | undefined
+
+const filteredWells = computed(() => {
+  const word = keyword.value.trim().toLowerCase()
+  if (!word) return wells.value
+  return wells.value.filter((well) =>
+    `${well.well ?? ''} ${well.uwi ?? ''}`.toLowerCase().includes(word)
+  )
+})
+
+const selectedWell = computed(() => {
+  return wells.value.find((well) => well.id === selectedWellId.value) ?? dataset.value?.wellInfo
+})
+
+const originalCount = computed(() => dataset.value?.depths?.length ?? 0)
+const depthRange = computed(() => {
+  const depths = dataset.value?.depths ?? []
+  let min = Number.POSITIVE_INFINITY
+  let max = Number.NEGATIVE_INFINITY
+  for (const rawDepth of depths) {
+    const depth = Number(rawDepth)
+    if (!Number.isFinite(depth)) continue
+    if (depth < min) min = depth
+    if (depth > max) max = depth
+  }
+  if (!Number.isFinite(min) || !Number.isFinite(max)) return '--'
+  return `${min.toFixed(2)} – ${max.toFixed(2)} m`
+})
+
+function getWellName(well?: DigitalWell) {
+  return well?.well || well?.uwi || (well?.id ? `井 ${well.id}` : '--')
+}
+
+/**
+ * 按深度等距寻找最近点,只生成一次索引;所有曲线严格复用同一组索引。
+ * 这样既适配不等间距深度,也不会发生曲线和深度错位。
+ */
+function createDepthSampleIndices(depths: number[], limit: number) {
+  const valid = depths
+    .map((depth, index) => ({ depth: Number(depth), index }))
+    .filter((item) => Number.isFinite(item.depth))
+  if (valid.length <= limit) return valid.map((item) => item.index)
+
+  const first = valid[0]
+  const last = valid[valid.length - 1]
+  const ascending = last.depth >= first.depth
+  const result = new Set<number>([first.index, last.index])
+  let cursor = 0
+
+  for (let point = 1; point < limit - 1; point += 1) {
+    const target = first.depth + ((last.depth - first.depth) * point) / (limit - 1)
+    while (cursor < valid.length - 2) {
+      const currentDistance = Math.abs(valid[cursor].depth - target)
+      const nextDistance = Math.abs(valid[cursor + 1].depth - target)
+      const passedTarget = ascending
+        ? valid[cursor + 1].depth > target
+        : valid[cursor + 1].depth < target
+      if (nextDistance > currentDistance && passedTarget) break
+      cursor += 1
+    }
+    result.add(valid[cursor].index)
+  }
+
+  return Array.from(result).sort((a, b) => a - b)
+}
+
+function curveValue(curve: CurveConfig, index: number): number | null {
+  const raw = (dataset.value?.[curve.key] as number[] | undefined)?.[index]
+  const value = Number(raw)
+  if (!Number.isFinite(value) || value <= -900) return null
+  // SW 接口为 0~1 小数时换算成用户给定的 20~100 显示范围。
+  if (curve.key === 'sws' && Math.abs(value) <= 1.5) return value * 100
+  return value
+}
+
+function formatNumber(value: unknown) {
+  const number = Number(value)
+  if (!Number.isFinite(number)) return '--'
+  return Math.abs(number) >= 100
+    ? number.toFixed(1)
+    : number.toFixed(3).replace(/0+$/, '').replace(/\.$/, '')
+}
+
+function buildChartOption(): echarts.EChartsOption {
+  const depths = dataset.value?.depths ?? []
+  const indices = sampledIndices.value
+  const sampledDepths = indices.map((index) => Number(depths[index])).filter(Number.isFinite)
+  const minDepth = sampledDepths.length ? Math.min(...sampledDepths) : 0
+  const maxDepth = sampledDepths.length ? Math.max(...sampledDepths) : 1
+
+  const xAxis: any[] = curves.map((curve) => ({
+    type: 'value',
+    gridIndex: curve.track,
+    min: curve.min,
+    max: curve.max,
+    inverse: curve.inverse,
+    show: false,
+    splitNumber: 5
+  }))
+
+  const yAxis: any[] = trackLayout.map((_, track) => ({
+    type: 'value' as const,
+    gridIndex: track,
+    min: minDepth,
+    max: maxDepth,
+    inverse: true,
+    axisLine: { show: true, lineStyle: { color: '#64748b', width: 1 } },
+    axisTick: { show: track === 0, length: 5, lineStyle: { color: '#64748b' } },
+    axisLabel: {
+      show: track === 0,
+      margin: 9,
+      color: '#334155',
+      fontSize: 11,
+      formatter: (value: number) => value.toFixed(1)
+    },
+    splitLine: { show: true, lineStyle: { color: '#cbd5e1', width: 1 } },
+    minorTick: { show: true, splitNumber: 5 },
+    minorSplitLine: { show: true, lineStyle: { color: '#eef2f7' } },
+    axisPointer: {
+      show: true,
+      snap: true,
+      label: {
+        show: track === 0,
+        formatter: ({ value }: { value: number }) => `${Number(value).toFixed(2)} m`
+      }
+    }
+  }))
+
+  const series = curves.map((curve, curveIndex) => ({
+    name: curve.name,
+    type: 'line' as const,
+    xAxisIndex: curveIndex,
+    yAxisIndex: curve.track,
+    data: indices.map((index) => [curveValue(curve, index), Number(depths[index])]),
+    showSymbol: false,
+    symbol: 'none',
+    connectNulls: false,
+    animation: false,
+    clip: true,
+    progressive: 4000,
+    progressiveThreshold: 8000,
+    lineStyle: { color: curve.color, width: 1.25, type: curve.lineType ?? 'solid' },
+    emphasis: { disabled: true }
+  }))
+
+  return {
+    animation: false,
+    backgroundColor: '#ffffff',
+    grid: trackLayout.map((layout) => ({
+      ...layout,
+      top: 8,
+      bottom: 32,
+      containLabel: false,
+      show: true,
+      borderColor: '#64748b',
+      borderWidth: 1
+    })),
+    xAxis,
+    yAxis,
+    series,
+    axisPointer: {
+      link: [{ yAxisIndex: [0, 1, 2, 3] }],
+      lineStyle: { color: '#ef4444', type: 'dashed' }
+    },
+    tooltip: {
+      trigger: 'axis',
+      confine: true,
+      backgroundColor: 'rgba(15, 23, 42, .94)',
+      borderWidth: 0,
+      textStyle: { color: '#f8fafc', fontSize: 12 },
+      // 测井图以深度为主轴。明确使用 y 轴触发,避免 NPHI 等重复值
+      // 被 ECharts 按相同 x 值一次性全部收进 tooltip。
+      axisPointer: { type: 'line', axis: 'y' },
+      formatter: (params: any) => {
+        const rows = Array.isArray(params) ? params : [params]
+        const uniqueRows = Array.from(
+          new Map(rows.map((item: any) => [item.seriesName, item])).values()
+        ) as any[]
+        const depth = uniqueRows[0]?.value?.[1]
+        const content = uniqueRows
+          .map((item: any) => {
+            const curve = curves.find((entry) => entry.name === item.seriesName)
+            const value = item.value?.[0]
+            return `<div style="display:flex;justify-content:space-between;gap:24px;margin-top:5px"><span><i style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${item.color};margin-right:7px"></i>${item.seriesName}</span><b>${formatNumber(value)} ${curve?.unit ?? ''}</b></div>`
+          })
+          .join('')
+        return `<div style="font-weight:600;margin-bottom:3px">深度 ${formatNumber(depth)} m</div>${content}`
+      }
+    },
+    dataZoom: [
+      {
+        type: 'inside',
+        yAxisIndex: [0, 1, 2, 3],
+        filterMode: 'none',
+        zoomOnMouseWheel: true,
+        moveOnMouseMove: true,
+        moveOnMouseWheel: false
+      },
+      {
+        type: 'slider',
+        yAxisIndex: [0, 1, 2, 3],
+        filterMode: 'none',
+        right: 4,
+        top: 8,
+        bottom: 32,
+        width: 14,
+        showDetail: false,
+        borderColor: '#cbd5e1',
+        fillerColor: 'rgba(37, 99, 235, .14)',
+        handleSize: 14
+      }
+    ]
+  }
+}
+
+function renderChart() {
+  if (!chartRef.value) return
+  if (!chart.value) chart.value = echarts.init(chartRef.value)
+  chart.value.setOption(buildChartOption(), true)
+}
+
+function updateSampling() {
+  sampledIndices.value = createDepthSampleIndices(dataset.value?.depths ?? [], SAMPLE_LIMIT)
+  nextTick(renderChart)
+}
+
+async function loadWellData(wellId?: number) {
+  if (!wellId) return
+  dataLoading.value = true
+  try {
+    dataset.value = await DigitalApi.getWellLogDataset(wellId)
+    updateSampling()
+  } catch (error) {
+    dataset.value = undefined
+    sampledIndices.value = []
+    renderChart()
+    message.error('测井数据加载失败,请稍后重试')
+  } finally {
+    dataLoading.value = false
+  }
+}
+
+async function loadWells() {
+  listLoading.value = true
+  let initialWellId: number | undefined
+  try {
+    const result = await DigitalApi.getWellList()
+    wells.value = result?.list ?? []
+    if (!selectedWellId.value && wells.value.length) selectedWellId.value = wells.value[0].id
+    initialWellId = selectedWellId.value
+  } catch (error) {
+    message.error('井列表加载失败,请稍后重试')
+  } finally {
+    listLoading.value = false
+  }
+
+  if (initialWellId) await loadWellData(initialWellId)
+}
+
+function selectWell(well: DigitalWell) {
+  if (well.id === selectedWellId.value) return
+  selectedWellId.value = well.id
+  loadWellData(well.id)
+}
+
+onMounted(async () => {
+  resizeObserver = new ResizeObserver(() => chart.value?.resize())
+  if (chartRef.value) resizeObserver.observe(chartRef.value)
+  renderChart()
+  await loadWells()
+})
+
+onBeforeUnmount(() => {
+  resizeObserver?.disconnect()
+  chart.value?.dispose()
+})
+</script>
+
+<template>
+  <div
+    class="digital-shell h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <aside class="well-panel">
+      <div class="panel-title">
+        <div>
+          <div class="eyebrow">WELL DIRECTORY</div>
+          <h2>井目录</h2>
+        </div>
+        <span class="well-count">{{ wells.length }}</span>
+      </div>
+
+      <el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索井名 / UWI" />
+
+      <div v-loading="listLoading" class="well-list">
+        <button
+          v-for="well in filteredWells"
+          :key="well.id"
+          type="button"
+          class="well-item"
+          :class="{ active: selectedWellId === well.id }"
+          @click="selectWell(well)">
+          <span class="well-status"></span>
+          <span class="well-text">
+            <strong>{{ getWellName(well) }}</strong>
+            <small>{{ well.uwi || `ID ${well.id}` }}</small>
+          </span>
+          <span class="well-depth">{{ well.stop ? `${well.stop.toFixed(0)}m` : '' }}</span>
+        </button>
+        <el-empty
+          v-if="!listLoading && !filteredWells.length"
+          :image-size="70"
+          description="暂无匹配井" />
+      </div>
+
+      <div v-if="selectedWell" class="well-summary">
+        <div class="summary-caption">当前井信息</div>
+        <dl>
+          <div
+            ><dt>井名</dt><dd>{{ getWellName(selectedWell) }}</dd></div
+          >
+          <div
+            ><dt>UWI</dt><dd>{{ selectedWell.uwi || '--' }}</dd></div
+          >
+          <div
+            ><dt>深度段</dt><dd>{{ depthRange }}</dd></div
+          >
+          <div
+            ><dt>采样间隔</dt><dd>{{ selectedWell.step ?? '--' }} m</dd></div
+          >
+        </dl>
+      </div>
+    </aside>
+
+    <main class="workspace">
+      <section v-loading="dataLoading" class="log-board">
+        <div class="track-header">
+          <div v-for="(group, track) in trackCurves" :key="track" class="track-card">
+            <div v-for="curve in group" :key="curve.key" class="curve-scale">
+              <div class="curve-name" :style="{ color: curve.color }">
+                <i :style="{ backgroundColor: curve.color }"></i>{{ curve.name }}
+              </div>
+              <div class="scale-row"
+                ><span>{{ curve.inverse ? curve.max : curve.min }}</span
+                ><b>{{ curve.unit }}</b
+                ><span>{{ curve.inverse ? curve.min : curve.max }}</span></div
+              >
+            </div>
+          </div>
+        </div>
+        <div class="depth-caption">DEPTH<br /><span>m</span></div>
+        <div ref="chartRef" class="log-chart"></div>
+        <el-empty
+          v-if="!dataLoading && !originalCount"
+          class="chart-empty"
+          description="请选择有测井数据的井" />
+      </section>
+    </main>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.digital-shell {
+  --navy: #0f2742;
+
+  display: flex;
+  width: 100%;
+  overflow: hidden;
+  color: #1e293b;
+  background: #e8edf3;
+  border: 1px solid #cbd5e1;
+  border-radius: 8px;
+}
+
+.eyebrow {
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 1.5px;
+  color: #64748b;
+}
+
+.well-panel {
+  display: flex;
+  flex: 0 0 238px;
+  flex-direction: column;
+  gap: 14px;
+  padding: 18px 14px;
+  background: #f8fafc;
+  border-right: 1px solid #cbd5e1;
+}
+
+.panel-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.panel-title h2 {
+  margin: 3px 0 0;
+  font-size: 20px;
+  color: var(--navy);
+}
+
+.well-count {
+  min-width: 30px;
+  padding: 5px 8px;
+  font-size: 12px;
+  font-weight: 700;
+  color: #fff;
+  text-align: center;
+  background: var(--navy);
+  border-radius: 12px;
+}
+
+.well-list {
+  flex: 1;
+  min-height: 0;
+  padding-right: 3px;
+  overflow-y: auto;
+}
+
+.well-item {
+  display: flex;
+  width: 100%;
+  padding: 10px;
+  margin-bottom: 4px;
+  color: #334155;
+  text-align: left;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  border-radius: 6px;
+  transition: 0.15s ease;
+  align-items: center;
+  gap: 9px;
+}
+
+.well-item:hover {
+  background: #e9eff6;
+}
+
+.well-item.active {
+  color: #0f3d69;
+  background: #dce9f6;
+  box-shadow: inset 3px 0 #2563eb;
+}
+
+.well-status {
+  width: 7px;
+  height: 7px;
+  flex: none;
+  background: #22c55e;
+  border-radius: 50%;
+  box-shadow: 0 0 0 3px rgb(34 197 94 / 12%);
+}
+
+.well-text {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  flex-direction: column;
+}
+
+.well-text strong {
+  overflow: hidden;
+  font-size: 13px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.well-text small {
+  margin-top: 2px;
+  font-size: 10px;
+  color: #94a3b8;
+}
+
+.well-depth {
+  font-family: monospace;
+  font-size: 10px;
+  color: #64748b;
+}
+
+.well-summary {
+  padding: 13px;
+  background: #eef3f8;
+  border: 1px solid #d8e0e9;
+  border-radius: 6px;
+}
+
+.summary-caption {
+  margin-bottom: 8px;
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 1px;
+  color: #64748b;
+}
+
+.well-summary dl {
+  margin: 0;
+}
+
+.well-summary dl div {
+  display: flex;
+  justify-content: space-between;
+  gap: 10px;
+  padding: 4px 0;
+  font-size: 11px;
+}
+
+.well-summary dt {
+  color: #64748b;
+}
+
+.well-summary dd {
+  margin: 0;
+  overflow: hidden;
+  font-weight: 600;
+  color: #1e293b;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.workspace {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  flex-direction: column;
+  padding: 14px;
+}
+
+.log-board {
+  position: relative;
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  flex-direction: column;
+  overflow: hidden;
+  background: #fff;
+  border: 1px solid #aebbc9;
+}
+
+.track-header {
+  display: grid;
+  flex: none;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 1.15%;
+  margin: 0 6% 0 7%;
+  background: #64748b;
+  border-right: 1px solid #64748b;
+  border-left: 1px solid #64748b;
+}
+
+.track-card {
+  min-width: 0;
+  background: #fff;
+  border-top: 1px solid #64748b;
+}
+
+.curve-scale {
+  padding: 5px 7px 6px;
+  border-bottom: 1px dotted #cbd5e1;
+}
+
+.curve-name {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  font-weight: 800;
+}
+
+.curve-name i {
+  width: 18px;
+  height: 2px;
+  margin-right: 6px;
+}
+
+.scale-row {
+  display: flex;
+  margin-top: 3px;
+  font-family: monospace;
+  font-size: 11px;
+  color: #334155;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.scale-row b {
+  font-weight: 500;
+  color: #64748b;
+}
+
+.depth-caption {
+  position: absolute;
+  top: 9px;
+  left: 1%;
+  z-index: 2;
+  width: 5%;
+  font-size: 13px;
+  font-weight: 800;
+  line-height: 1.4;
+  color: #334155;
+  text-align: center;
+}
+
+.depth-caption span {
+  font-size: 11px;
+  font-weight: 500;
+  color: #64748b;
+}
+
+.log-chart {
+  width: 100%;
+  min-height: 360px;
+  flex: 1;
+}
+
+.chart-empty {
+  position: absolute;
+  inset: 150px 0 0;
+  background: rgb(255 255 255 / 85%);
+}
+
+@media (width <= 1200px) {
+  .well-panel {
+    flex-basis: 205px;
+  }
+}
+</style>

+ 58 - 0
src/views/iot/commonThing/ThingModelEvent.vue

@@ -0,0 +1,58 @@
+<!-- 产品的物模型表单(event 项) -->
+<template>
+  <el-form-item
+    :rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
+    label="事件类型"
+    prop="event.type"
+  >
+    <el-radio-group v-model="thingModelEvent.type">
+      <el-radio
+        v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
+        :key="eventType.value"
+        :value="eventType.value"
+      >
+        {{ eventType.label }}
+      </el-radio>
+    </el-radio-group>
+  </el-form-item>
+  <el-form-item label="输出参数">
+    <ThingModelInputOutputParam
+      v-model="thingModelEvent.outputParams"
+      :direction="IoTThingModelParamDirectionEnum.OUTPUT"
+    />
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
+import { useVModel } from '@vueuse/core'
+import { ThingModelEvent } from '@/api/iot/thingmodel'
+import { isEmpty } from '@/utils/is'
+import {
+  IoTThingModelEventTypeEnum,
+  IoTThingModelParamDirectionEnum
+} from '@/views/iot/utils/constants'
+
+/** IoT 物模型事件 */
+defineOptions({ name: 'ThingModelEvent' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<ThingModelEvent>
+
+// 默认选中,INFO 信息
+watch(
+  () => thingModelEvent.value.type,
+  (val: string) =>
+    isEmpty(val) && (thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value),
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 14 - 23
src/views/iot/thingmodel/ThingModelFormCommon.vue → src/views/iot/commonThing/ThingModelForm.vue

@@ -23,7 +23,7 @@
       <el-form-item label="标识符" prop="identifier">
         <el-input v-model="formData.identifier" placeholder="请输入标识符" />
       </el-form-item>
-      <!-- 属性配-->
+      <!-- 属性配�?-->
       <ThingModelProperty
         v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
         v-model="formData.property" />
@@ -46,38 +46,31 @@
     </el-form>
 
     <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确定</el-button>
+      <el-button @click="dialogVisible = false">取消</el-button>
     </template>
   </Dialog>
 </template>
 
 <script lang="ts" setup>
-import { ProductVO } from '@/api/iot/product/product'
 import ThingModelProperty from './ThingModelProperty.vue'
 import ThingModelService from './ThingModelService.vue'
 import ThingModelEvent from './ThingModelEvent.vue'
 import { ThingModelCommon, ThingModelData, ThingModelFormRules } from '@/api/iot/thingmodel'
-import {
-  IOT_PROVIDE_KEY,
-  IoTDataSpecsDataTypeEnum,
-  IoTThingModelTypeEnum
-} from '@/views/iot/utils/constants'
+import { IoTDataSpecsDataTypeEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
 import { cloneDeep } from 'lodash-es'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { isEmpty } from '@/utils/is'
 
-/** IoT 物模型数据表*/
+/** IoT 物模型数据表�?*/
 defineOptions({ name: 'IoTThingModelForm' })
 
-const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
-
-const { t } = useI18n() // 国际化
+const { t } = useI18n() // 国际�?
 const message = useMessage() // 消息弹窗
 
-const dialogVisible = ref(false) // 弹窗的是否展
-const dialogTitle = ref('') // 弹窗的标
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const dialogVisible = ref(false) // 弹窗的是否展�?
+const dialogTitle = ref('') // 弹窗的标�?
+const formLoading = ref(false) // 表单的加载中�?)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref<ThingModelData>({
   type: IoTThingModelTypeEnum.PROPERTY,
@@ -114,11 +107,11 @@ const open = async (type: string, id?: number) => {
           }
         }
       }
-      // 情况二:服务初始
+      // 情况二:服务初始�?
       if (isEmpty(formData.value.service)) {
         formData.value.service = {}
       }
-      // 情况三:事件初始
+      // 情况三:事件初始�?
       if (isEmpty(formData.value.event)) {
         formData.value.event = {}
       }
@@ -136,9 +129,7 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     const data = cloneDeep(formData.value) as ThingModelData
-    // // 信息补全
-    // data.productId = product!.value.id
-    // data.productKey = product!.value.productKey
+
     fillExtraAttributes(data)
     if (formType.value === 'create') {
       await ThingModelCommon.createCommonThingModel(data)
@@ -157,7 +148,7 @@ const submitForm = async () => {
 
 /** 填写额外的属性(处理不同类型的情况) */
 const fillExtraAttributes = (data: any) => {
-  // 属
+  // 属�?
   if (data.type === IoTThingModelTypeEnum.PROPERTY) {
     removeDataSpecs(data.property)
     data.dataType = data.property.dataType
@@ -186,7 +177,7 @@ const fillExtraAttributes = (data: any) => {
   }
 }
 
-/** 处理 dataSpecs 为空的情*/
+/** 处理 dataSpecs 为空的情�?*/
 const removeDataSpecs = (val: any) => {
   if (isEmpty(val.dataSpecs)) {
     delete val.dataSpecs

+ 151 - 0
src/views/iot/commonThing/ThingModelInputOutputParam.vue

@@ -0,0 +1,151 @@
+<!-- 产品的物模型表单(event、service 项里的参数) -->
+<template>
+  <div
+    v-for="(item, index) in thingModelParams"
+    :key="index"
+    class="w-1/1 param-item flex justify-between px-10px mb-10px"
+  >
+    <span>参数名称:{{ item.name }}</span>
+    <div class="btn">
+      <el-button link type="primary" @click="openParamForm(item)">编辑</el-button>
+      <el-divider direction="vertical" />
+      <el-button link type="danger" @click="deleteParamItem(index)">删除</el-button>
+    </div>
+  </div>
+  <el-button link type="primary" @click="openParamForm(null)">+新增参数</el-button>
+
+  <!-- param 表单 -->
+  <Dialog v-model="dialogVisible" title="新增参数" append-to-body>
+    <el-form
+      ref="paramFormRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="ThingModelFormRules"
+      label-width="100px"
+    >
+      <el-form-item label="参数名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入功能名称" />
+      </el-form-item>
+      <el-form-item label="标识符" prop="identifier">
+        <el-input v-model="formData.identifier" placeholder="请输入标识符" />
+      </el-form-item>
+      <!-- 属性配置 -->
+      <ThingModelProperty v-model="formData.property" is-params />
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelProperty from './ThingModelProperty.vue'
+import { isEmpty } from '@/utils/is'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+import { ThingModelFormRules } from '@/api/iot/thingmodel'
+
+/** 输入输出参数配置组件 */
+defineOptions({ name: 'ThingModelInputOutputParam' })
+
+const props = defineProps<{ modelValue: any; direction: string }>()
+const emits = defineEmits(['update:modelValue'])
+const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const paramFormRef = ref() // 表单 ref
+const formData = ref<any>({
+  dataType: IoTDataSpecsDataTypeEnum.INT,
+  property: {
+    dataType: IoTDataSpecsDataTypeEnum.INT,
+    dataSpecs: {
+      dataType: IoTDataSpecsDataTypeEnum.INT
+    }
+  }
+})
+
+/** 打开 param 表单 */
+const openParamForm = (val: any) => {
+  dialogVisible.value = true
+  resetForm()
+  if (isEmpty(val)) {
+    return
+  }
+  // 编辑时回显数据
+  formData.value = {
+    identifier: val.identifier,
+    name: val.name,
+    description: val.description,
+    property: {
+      dataType: val.dataType,
+      dataSpecs: val.dataSpecs,
+      dataSpecsList: val.dataSpecsList
+    }
+  }
+}
+
+/** 删除 param 项 */
+const deleteParamItem = (index: number) => {
+  thingModelParams.value.splice(index, 1)
+}
+
+/** 添加参数 */
+const submitForm = async () => {
+  // 初始化参数列表
+  if (isEmpty(thingModelParams.value)) {
+    thingModelParams.value = []
+  }
+  // 校验参数
+  await paramFormRef.value.validate()
+  try {
+    // 构建数据对象
+    const data = unref(formData)
+    const item = {
+      identifier: data.identifier,
+      name: data.name,
+      description: data.description,
+      dataType: data.property.dataType,
+      paraOrder: 0, // TODO @puhui999: 先写死默认看看后续
+      direction: props.direction,
+      dataSpecs:
+        !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
+          ? data.property.dataSpecs
+          : undefined,
+      dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList
+    }
+
+    // 新增或修改同 identifier 的参数
+    const existingIndex = thingModelParams.value.findIndex(
+      (spec) => spec.identifier === data.identifier
+    )
+    if (existingIndex > -1) {
+      thingModelParams.value[existingIndex] = item
+    } else {
+      thingModelParams.value.push(item)
+    }
+  } finally {
+    dialogVisible.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    dataType: IoTDataSpecsDataTypeEnum.INT,
+    property: {
+      dataType: IoTDataSpecsDataTypeEnum.INT,
+      dataSpecs: {
+        dataType: IoTDataSpecsDataTypeEnum.INT
+      }
+    }
+  }
+  paramFormRef.value?.resetFields()
+}
+</script>
+
+<style lang="scss" scoped>
+.param-item {
+  background-color: #e4f2fd;
+}
+</style>

+ 177 - 0
src/views/iot/commonThing/ThingModelProperty.vue

@@ -0,0 +1,177 @@
+<!-- 产品的物模型表单(property 项) -->
+<template>
+  <el-form-item
+    :rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
+    label="数据类型"
+    prop="property.dataType"
+  >
+    <el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
+      <!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) -->
+      <el-option
+        v-for="option in getDataTypeOptions2"
+        :key="option.value"
+        :label="`${option.value}(${option.label})`"
+        :value="option.value"
+      />
+    </el-select>
+  </el-form-item>
+  <!-- 数值型配置 -->
+  <ThingModelNumberDataSpecs
+    v-if="
+      [
+        IoTDataSpecsDataTypeEnum.INT,
+        IoTDataSpecsDataTypeEnum.DOUBLE,
+        IoTDataSpecsDataTypeEnum.FLOAT
+      ].includes(property.dataType || '')
+    "
+    v-model="property.dataSpecs"
+  />
+  <!-- 枚举型配置 -->
+  <ThingModelEnumDataSpecs
+    v-if="property.dataType === IoTDataSpecsDataTypeEnum.ENUM"
+    v-model="property.dataSpecsList"
+  />
+  <!-- 布尔型配置 -->
+  <el-form-item v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" label="布尔值">
+    <template v-for="(item, index) in property.dataSpecsList" :key="item.value">
+      <div class="flex items-center justify-start w-1/1 mb-5px">
+        <span>{{ item.value }}</span>
+        <span class="mx-2">-</span>
+        <el-form-item
+          :prop="`property.dataSpecsList[${index}].name`"
+          :rules="[
+            { required: true, message: '枚举描述不能为空' },
+            { validator: validateBoolName, trigger: 'blur' }
+          ]"
+          class="flex-1 mb-0"
+        >
+          <el-input
+            v-model="item.name"
+            :placeholder="`如:${item.value === 0 ? '关' : '开'}`"
+            class="w-255px!"
+          />
+        </el-form-item>
+      </div>
+    </template>
+  </el-form-item>
+  <!-- 文本型配置 -->
+  <el-form-item
+    v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
+    label="数据长度"
+    prop="property.dataSpecs.length"
+  >
+    <el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
+      <template #append>字节</template>
+    </el-input>
+  </el-form-item>
+  <!-- 时间型配置 -->
+  <el-form-item
+    v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
+    label="时间格式"
+    prop="date"
+  >
+    <el-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" />
+  </el-form-item>
+  <!-- 数组型配置-->
+  <ThingModelArrayDataSpecs
+    v-if="property.dataType === IoTDataSpecsDataTypeEnum.ARRAY"
+    v-model="property.dataSpecs"
+  />
+  <!-- Struct 型配置-->
+  <ThingModelStructDataSpecs
+    v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
+    v-model="property.dataSpecsList"
+  />
+  <el-form-item v-if="!isStructDataSpecs && !isParams" label="读写类型" prop="property.accessMode">
+    <el-radio-group v-model="property.accessMode">
+      <el-radio
+        v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
+        :key="accessMode.value"
+        :label="accessMode.value"
+      >
+        {{ accessMode.label }}
+      </el-radio>
+    </el-radio-group>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import {
+  ThingModelArrayDataSpecs,
+  ThingModelEnumDataSpecs,
+  ThingModelNumberDataSpecs,
+  ThingModelStructDataSpecs
+} from './dataSpecs'
+import { ThingModelProperty, validateBoolName } from '@/api/iot/thingmodel'
+import { isEmpty } from '@/utils/is'
+import {
+  getDataTypeOptions,
+  IoTDataSpecsDataTypeEnum,
+  IoTThingModelAccessModeEnum
+} from '@/views/iot/utils/constants'
+
+/** IoT 物模型属性 */
+defineOptions({ name: 'ThingModelProperty' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean; isParams?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty>
+const getDataTypeOptions2 = computed(() => {
+  if (!props.isStructDataSpecs) {
+    return getDataTypeOptions()
+  }
+  const excludedTypes = [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY]
+  return getDataTypeOptions().filter((item: any) => !excludedTypes.includes(item.value))
+}) // 获得数据类型列表
+
+/** 属性值的数据类型切换时初始化相关数据 */
+const handleChange = (dataType: any) => {
+  property.value.dataSpecs = {}
+  property.value.dataSpecsList = []
+  // 不是列表型数据才设置 dataSpecs.dataType
+  ![
+    IoTDataSpecsDataTypeEnum.ENUM,
+    IoTDataSpecsDataTypeEnum.BOOL,
+    IoTDataSpecsDataTypeEnum.STRUCT
+  ].includes(dataType) && (property.value.dataSpecs.dataType = dataType)
+  switch (dataType) {
+    case IoTDataSpecsDataTypeEnum.ENUM:
+      property.value.dataSpecsList.push({
+        dataType: IoTDataSpecsDataTypeEnum.ENUM,
+        name: '', // 枚举项的名称
+        value: undefined // 枚举值
+      })
+      break
+    case IoTDataSpecsDataTypeEnum.BOOL:
+      for (let i = 0; i < 2; i++) {
+        property.value.dataSpecsList.push({
+          dataType: IoTDataSpecsDataTypeEnum.BOOL,
+          name: '', // 布尔值的名称
+          value: i // 布尔值
+        })
+      }
+      break
+  }
+}
+
+/** 默认选中读写 */
+watch(
+  () => property.value.accessMode,
+  (val: string) => {
+    if (props.isStructDataSpecs || props.isParams) {
+      return
+    }
+    isEmpty(val) && (property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value)
+  },
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 64 - 0
src/views/iot/commonThing/ThingModelService.vue

@@ -0,0 +1,64 @@
+<!-- 产品的物模型表单(service 项) -->
+<template>
+  <el-form-item
+    :rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
+    label="调用方式"
+    prop="service.callType"
+  >
+    <el-radio-group v-model="service.callType">
+      <el-radio
+        v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)"
+        :key="callType.value"
+        :value="callType.value"
+      >
+        {{ callType.label }}
+      </el-radio>
+    </el-radio-group>
+  </el-form-item>
+  <el-form-item label="输入参数">
+    <ThingModelInputOutputParam
+      v-model="service.inputParams"
+      :direction="IoTThingModelParamDirectionEnum.INPUT"
+    />
+  </el-form-item>
+  <el-form-item label="输出参数">
+    <ThingModelInputOutputParam
+      v-model="service.outputParams"
+      :direction="IoTThingModelParamDirectionEnum.OUTPUT"
+    />
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
+import { useVModel } from '@vueuse/core'
+import { ThingModelService } from '@/api/iot/thingmodel'
+import { isEmpty } from '@/utils/is'
+import {
+  IoTThingModelParamDirectionEnum,
+  IoTThingModelServiceCallTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** IoT 物模型服务 */
+defineOptions({ name: 'ThingModelService' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>
+
+/** 默认选中,ASYNC 异步 */
+watch(
+  () => service.value.callType,
+  (val: string) =>
+    isEmpty(val) && (service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value),
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 49 - 0
src/views/iot/commonThing/ThingModelTSL.vue

@@ -0,0 +1,49 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <JsonEditor
+      v-model="thingModelTSL"
+      :mode="viewMode === 'editor' ? 'code' : 'view'"
+      height="600px" />
+    <template #footer>
+      <el-radio-group v-model="viewMode" size="small">
+        <el-radio-button label="code">代码视图</el-radio-button>
+        <el-radio-button label="editor">编辑器视图</el-radio-button>
+      </el-radio-group>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import hljs from 'highlight.js' // 导入代码高亮文件
+import 'highlight.js/styles/github.css' // 导入代码高亮样式
+import json from 'highlight.js/lib/languages/json'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import { ProductVO } from '@/api/iot/product/product'
+import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
+
+defineOptions({ name: 'ThingModelTSL' })
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('物模型 TSL') // 弹窗的标题
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
+const viewMode = ref('code') // 查看模式:code-代码视图,editor-编辑器视图
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+}
+defineExpose({ open })
+
+/** 获取 TSL */
+const thingModelTSL = ref({})
+const getTsl = async () => {
+  thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product?.value?.id || 0)
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  // 注册代码高亮的各种语言
+  hljs.registerLanguage('json', json)
+  await getTsl()
+})
+</script>

+ 74 - 0
src/views/iot/commonThing/components/DataDefinition.vue

@@ -0,0 +1,74 @@
+<template>
+  <!-- 属�?-->
+  <template v-if="data.type === IoTThingModelTypeEnum.PROPERTY && data.property">
+    <!-- 非列表型:数�?-->
+    <div
+      v-if="
+        [
+          IoTDataSpecsDataTypeEnum.INT,
+          IoTDataSpecsDataTypeEnum.DOUBLE,
+          IoTDataSpecsDataTypeEnum.FLOAT
+        ].includes(data.property.dataType)
+      ">
+      取值范围:{{
+        `${data.property.dataSpecs?.min ?? '-'}~${data.property.dataSpecs?.max ?? '-'}`
+      }}
+    </div>
+    <!-- 非列表型:文�?-->
+    <div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType">
+      数据长度:{{ data.property.dataSpecs?.length ?? '-' }}
+    </div>
+    <!-- 列表�? 数组、结构、时间(特殊�?-->
+    <div
+      v-if="
+        [
+          IoTDataSpecsDataTypeEnum.ARRAY,
+          IoTDataSpecsDataTypeEnum.STRUCT,
+          IoTDataSpecsDataTypeEnum.DATE
+        ].includes(data.property.dataType)
+      ">
+      -
+    </div>
+    <!-- 列表�? 布尔值、枚�?-->
+    <div
+      v-if="
+        [IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
+          data.property.dataType
+        )
+      ">
+      <div>
+        {{ IoTDataSpecsDataTypeEnum.BOOL === data.property.dataType ? '布尔值' : '枚举值' }}:
+      </div>
+      <div v-for="item in data.property.dataSpecsList || []" :key="item.value">
+        {{ `${item.name}-${item.value}` }}
+      </div>
+    </div>
+  </template>
+  <!-- 服务 -->
+  <div v-if="data.type === IoTThingModelTypeEnum.SERVICE">
+    调用方式:{{
+      data.service?.callType ? getThingModelServiceCallTypeLabel(data.service.callType) : '-'
+    }}
+  </div>
+  <!-- 事件 -->
+  <div v-if="data.type === IoTThingModelTypeEnum.EVENT">
+    事件类型:{{ data.event?.type ? getEventTypeLabel(data.event.type) : '-' }}
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ThingModelData } from '@/api/iot/thingmodel'
+import {
+  getEventTypeLabel,
+  getThingModelServiceCallTypeLabel,
+  IoTDataSpecsDataTypeEnum,
+  IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** 数据定义展示组件 */
+defineOptions({ name: 'DataDefinition' })
+
+defineProps<{ data: ThingModelData }>()
+</script>
+
+<style lang="scss" scoped></style>

+ 3 - 0
src/views/iot/commonThing/components/index.ts

@@ -0,0 +1,3 @@
+import DataDefinition from './DataDefinition.vue'
+
+export { DataDefinition }

+ 56 - 0
src/views/iot/commonThing/dataSpecs/ThingModelArrayDataSpecs.vue

@@ -0,0 +1,56 @@
+<!-- dataType:array 数组类型 -->
+<template>
+  <el-form-item label="元素类型" prop="property.dataSpecs.childDataType">
+    <el-radio-group v-model="dataSpecs.childDataType" @change="handleChange">
+      <template v-for="item in getDataTypeOptions()" :key="item.value">
+        <el-radio
+          v-if="
+            !(
+              [
+                IoTDataSpecsDataTypeEnum.ENUM,
+                IoTDataSpecsDataTypeEnum.ARRAY,
+                IoTDataSpecsDataTypeEnum.DATE
+              ] as any[]
+            ).includes(item.value)
+          "
+          :value="item.value"
+          class="w-1/3"
+        >
+          {{ `${item.value}(${item.label})` }}
+        </el-radio>
+      </template>
+    </el-radio-group>
+  </el-form-item>
+  <el-form-item label="元素个数" prop="property.dataSpecs.size">
+    <el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" />
+  </el-form-item>
+  <!-- Struct 型配置-->
+  <ThingModelStructDataSpecs
+    v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
+    v-model="dataSpecs.dataSpecsList"
+  />
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
+import { getDataTypeOptions, IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+
+/** 数组型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThingModelArrayDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>
+
+/** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */
+const handleChange = (val: string) => {
+  if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
+    return
+  }
+
+  dataSpecs.value.dataSpecsList = []
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 160 - 0
src/views/iot/commonThing/dataSpecs/ThingModelEnumDataSpecs.vue

@@ -0,0 +1,160 @@
+<!-- dataType:enum 数组类型 -->
+<template>
+  <el-form-item
+    :rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
+    label="枚举项"
+  >
+    <div class="flex flex-col">
+      <div class="flex items-center">
+        <span class="flex-1"> 参数值 </span>
+        <span class="flex-1"> 参数描述 </span>
+      </div>
+      <div
+        v-for="(item, index) in dataSpecsList"
+        :key="index"
+        class="flex items-center justify-between mb-5px"
+      >
+        <el-form-item
+          :prop="`property.dataSpecsList[${index}].value`"
+          :rules="[
+            { required: true, message: '枚举值不能为空' },
+            { validator: validateEnumValue, trigger: 'blur' }
+          ]"
+          class="flex-1 mb-0"
+        >
+          <el-input v-model="item.value" placeholder="请输入枚举值,如'0'" />
+        </el-form-item>
+        <span class="mx-2">~</span>
+        <el-form-item
+          :prop="`property.dataSpecsList[${index}].name`"
+          :rules="[
+            { required: true, message: '枚举描述不能为空' },
+            { validator: validateEnumName, trigger: 'blur' }
+          ]"
+          class="flex-1 mb-0"
+        >
+          <el-input v-model="item.name" placeholder="对该枚举项的描述" />
+        </el-form-item>
+        <el-button class="ml-10px" link type="primary" @click="deleteEnum(index)">删除</el-button>
+      </div>
+      <el-button link type="primary" @click="addEnum">+添加枚举项</el-button>
+    </div>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+import { DataSpecsEnumOrBoolData } from '@/api/iot/thingmodel'
+
+/** 枚举型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThingModelEnumDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolData[]>
+const message = useMessage()
+
+/** 添加枚举项 */
+const addEnum = () => {
+  dataSpecsList.value.push({
+    dataType: IoTDataSpecsDataTypeEnum.ENUM,
+    name: '', // 枚举项的名称
+    value: undefined // 枚举值
+  })
+}
+
+/** 删除枚举项 */
+const deleteEnum = (index: number) => {
+  if (dataSpecsList.value.length === 1) {
+    message.warning('至少需要一个枚举项')
+    return
+  }
+  dataSpecsList.value.splice(index, 1)
+}
+
+/** 校验枚举值 */
+const validateEnumValue = (_: any, value: any, callback: any) => {
+  if (isEmpty(value)) {
+    callback(new Error('枚举值不能为空'))
+    return
+  }
+  if (isNaN(Number(value))) {
+    callback(new Error('枚举值必须是数字'))
+    return
+  }
+  // 检查枚举值是否重复
+  const values = dataSpecsList.value.map((item) => item.value)
+  if (values.filter((v) => v === value).length > 1) {
+    callback(new Error('枚举值不能重复'))
+    return
+  }
+  callback()
+}
+
+/** 校验枚举描述 */
+const validateEnumName = (_: any, value: string, callback: any) => {
+  if (isEmpty(value)) {
+    callback(new Error('枚举描述不能为空'))
+    return
+  }
+  // 检查开头字符
+  if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) {
+    callback(new Error('枚举描述必须以中文、英文字母或数字开头'))
+    return
+  }
+  // 检查整体格式
+  if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) {
+    callback(new Error('枚举描述只能包含中文、英文字母、数字、下划线和短划线'))
+    return
+  }
+  // 检查长度(一个中文算一个字符)
+  if (value.length > 20) {
+    callback(new Error('枚举描述长度不能超过20个字符'))
+    return
+  }
+  callback()
+}
+
+/** 校验整个枚举列表 */
+const validateEnumList = (_: any, __: any, callback: any) => {
+  if (isEmpty(dataSpecsList.value)) {
+    callback(new Error('请至少添加一个枚举项'))
+    return
+  }
+
+  // 检查是否存在空值
+  const hasEmptyValue = dataSpecsList.value.some(
+    (item) => isEmpty(item.value) || isEmpty(item.name)
+  )
+  if (hasEmptyValue) {
+    callback(new Error('存在未填写的枚举值或描述'))
+    return
+  }
+
+  // 检查枚举值是否都是数字
+  const hasInvalidNumber = dataSpecsList.value.some((item) => isNaN(Number(item.value)))
+  if (hasInvalidNumber) {
+    callback(new Error('存在非数字的枚举值'))
+    return
+  }
+
+  // 检查是否有重复的枚举值
+  const values = dataSpecsList.value.map((item) => item.value)
+  const uniqueValues = new Set(values)
+  if (values.length !== uniqueValues.size) {
+    callback(new Error('存在重复的枚举值'))
+    return
+  }
+  callback()
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 139 - 0
src/views/iot/commonThing/dataSpecs/ThingModelNumberDataSpecs.vue

@@ -0,0 +1,139 @@
+<!-- dataType:number 数组类型 -->
+<template>
+  <el-form-item label="取值范围">
+    <div class="flex items-center justify-between">
+      <el-form-item
+        :rules="[
+          { required: true, message: '最小值不能为空' },
+          { validator: validateMin, trigger: 'blur' }
+        ]"
+        class="mb-0"
+        prop="property.dataSpecs.min"
+      >
+        <el-input v-model="dataSpecs.min" placeholder="请输入最小值" />
+      </el-form-item>
+      <span class="mx-2">~</span>
+      <el-form-item
+        :rules="[
+          { required: true, message: '最大值不能为空' },
+          { validator: validateMax, trigger: 'blur' }
+        ]"
+        class="mb-0"
+        prop="property.dataSpecs.max"
+      >
+        <el-input v-model="dataSpecs.max" placeholder="请输入最大值" />
+      </el-form-item>
+    </div>
+  </el-form-item>
+  <el-form-item
+    :rules="[
+      { required: true, message: '步长不能为空' },
+      { validator: validateStep, trigger: 'blur' }
+    ]"
+    label="步长"
+    prop="property.dataSpecs.step"
+  >
+    <el-input v-model="dataSpecs.step" placeholder="请输入步长" />
+  </el-form-item>
+  <el-form-item
+    :rules="[{ required: true, message: '请选择单位' }]"
+    label="单位"
+    prop="property.dataSpecs.unit"
+  >
+    <el-select
+      :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''"
+      filterable
+      placeholder="请选择单位"
+      class="w-1/1"
+      @change="unitChange"
+    >
+      <el-option
+        v-for="(item, index) in getStrDictOptions(DICT_TYPE.IOT_THING_MODEL_UNIT)"
+        :key="index"
+        :label="item.label + '-' + item.value"
+        :value="item.label + '-' + item.value"
+      />
+    </el-select>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DataSpecsNumberData } from '@/api/iot/thingmodel'
+
+/** 数值型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThingModelNumberDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<DataSpecsNumberData>
+
+/** 单位发生变化时触发 */
+const unitChange = (UnitSpecs: string) => {
+  const [unitName, unit] = UnitSpecs.split('-')
+  dataSpecs.value.unitName = unitName
+  dataSpecs.value.unit = unit
+}
+
+/** 校验最小值 */
+const validateMin = (_: any, __: any, callback: any) => {
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+  if (isNaN(min)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+  if (max !== undefined && !isNaN(max) && min >= max) {
+    callback(new Error('最小值必须小于最大值'))
+    return
+  }
+
+  callback()
+}
+
+/** 校验最大值 */
+const validateMax = (_: any, __: any, callback: any) => {
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+  if (isNaN(max)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+  if (min !== undefined && !isNaN(min) && max <= min) {
+    callback(new Error('最大值必须大于最小值'))
+    return
+  }
+
+  callback()
+}
+
+/** 校验步长 */
+const validateStep = (_: any, __: any, callback: any) => {
+  const step = Number(dataSpecs.value.step)
+  if (isNaN(step)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+  if (step <= 0) {
+    callback(new Error('步长必须大于0'))
+    return
+  }
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+  if (!isNaN(min) && !isNaN(max) && step > max - min) {
+    callback(new Error('步长不能大于最大值和最小值的差值'))
+    return
+  }
+
+  callback()
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 167 - 0
src/views/iot/commonThing/dataSpecs/ThingModelStructDataSpecs.vue

@@ -0,0 +1,167 @@
+<!-- dataType:struct 数组类型 -->
+<template>
+  <!-- struct 数据展示 -->
+  <el-form-item
+    :rules="[{ required: true, validator: validateList, trigger: 'change' }]"
+    label="JSON 对象"
+  >
+    <div
+      v-for="(item, index) in dataSpecsList"
+      :key="index"
+      class="w-1/1 struct-item flex justify-between px-10px mb-10px"
+    >
+      <span>参数名称:{{ item.name }}</span>
+      <div class="btn">
+        <el-button link type="primary" @click="openStructForm(item)">编辑</el-button>
+        <el-divider direction="vertical" />
+        <el-button link type="danger" @click="deleteStructItem(index)">删除</el-button>
+      </div>
+    </div>
+    <el-button link type="primary" @click="openStructForm(null)">+新增参数</el-button>
+  </el-form-item>
+
+  <!-- struct 表单 -->
+  <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body>
+    <el-form
+      ref="structFormRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="ThingModelFormRules"
+      label-width="100px"
+    >
+      <el-form-item label="参数名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入功能名称" />
+      </el-form-item>
+      <el-form-item label="标识符" prop="identifier">
+        <el-input v-model="formData.identifier" placeholder="请输入标识符" />
+      </el-form-item>
+      <!-- 属性配置 -->
+      <ThingModelProperty v-model="formData.property" is-struct-data-specs />
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelProperty from '../ThingModelProperty.vue'
+import { isEmpty } from '@/utils/is'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+import { ThingModelFormRules } from '@/api/iot/thingmodel'
+
+/** Struct 型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThingModelStructDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('新增参数') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const structFormRef = ref() // 表单 ref
+const formData = ref<any>({
+  property: {
+    dataType: IoTDataSpecsDataTypeEnum.INT,
+    dataSpecs: {
+      dataType: IoTDataSpecsDataTypeEnum.INT
+    }
+  }
+})
+
+/** 打开 struct 表单 */
+const openStructForm = (val: any) => {
+  dialogVisible.value = true
+  resetForm()
+  if (isEmpty(val)) {
+    return
+  }
+  // 编辑时回显数据
+  formData.value = {
+    identifier: val.identifier,
+    name: val.name,
+    description: val.description,
+    property: {
+      dataType: val.childDataType,
+      dataSpecs: val.dataSpecs,
+      dataSpecsList: val.dataSpecsList
+    }
+  }
+}
+
+/** 删除 struct 项 */
+const deleteStructItem = (index: number) => {
+  dataSpecsList.value.splice(index, 1)
+}
+
+/** 添加参数 */
+const submitForm = async () => {
+  await structFormRef.value.validate()
+
+  try {
+    const data = unref(formData)
+    // 构建数据对象
+    const item = {
+      identifier: data.identifier,
+      name: data.name,
+      description: data.description,
+      dataType: IoTDataSpecsDataTypeEnum.STRUCT,
+      childDataType: data.property.dataType,
+      dataSpecs:
+        !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
+          ? data.property.dataSpecs
+          : undefined,
+      dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList
+    }
+
+    // 新增或修改同 identifier 的参数
+    const existingIndex = dataSpecsList.value.findIndex(
+      (spec) => spec.identifier === data.identifier
+    )
+    if (existingIndex > -1) {
+      dataSpecsList.value[existingIndex] = item
+    } else {
+      dataSpecsList.value.push(item)
+    }
+  } finally {
+    dialogVisible.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    property: {
+      dataType: IoTDataSpecsDataTypeEnum.INT,
+      dataSpecs: {
+        dataType: IoTDataSpecsDataTypeEnum.INT
+      }
+    }
+  }
+  structFormRef.value?.resetFields()
+}
+
+/** 校验 struct 不能为空 */
+const validateList = (_: any, __: any, callback: any) => {
+  if (isEmpty(dataSpecsList.value)) {
+    callback(new Error('struct 不能为空'))
+    return
+  }
+  callback()
+}
+
+/** 组件初始化 */
+onMounted(async () => {
+  await nextTick()
+  // 预防 dataSpecsList 空指针
+  isEmpty(dataSpecsList.value) && (dataSpecsList.value = [])
+})
+</script>
+
+<style lang="scss" scoped>
+.struct-item {
+  background-color: #e4f2fd;
+}
+</style>

+ 11 - 0
src/views/iot/commonThing/dataSpecs/index.ts

@@ -0,0 +1,11 @@
+import ThingModelEnumDataSpecs from './ThingModelEnumDataSpecs.vue'
+import ThingModelNumberDataSpecs from './ThingModelNumberDataSpecs.vue'
+import ThingModelArrayDataSpecs from './ThingModelArrayDataSpecs.vue'
+import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
+
+export {
+  ThingModelEnumDataSpecs,
+  ThingModelNumberDataSpecs,
+  ThingModelArrayDataSpecs,
+  ThingModelStructDataSpecs
+}

+ 94 - 121
src/views/iot/commonThing/index.vue

@@ -1,33 +1,20 @@
+<!-- 产品的物模型列表 -->
 <template>
   <ContentWrap>
+    <!-- 搜索工作栏 -->
     <el-form
       ref="queryFormRef"
-      :model="queryParams"
       :inline="true"
+      :model="queryParams"
       class="-mb-15px"
-      label-width="80px">
-      <el-form-item label="功能标识" prop="identifier">
-        <el-input
-          v-model="queryParams.identifier"
-          class="!w-150px"
-          clearable
-          placeholder="请输入功能标识"
-          @keyup.enter="handleQuery" />
-      </el-form-item>
-      <el-form-item label="功能名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          class="!w-150px"
-          clearable
-          placeholder="请输入功能名称"
-          @keyup.enter="handleQuery" />
-      </el-form-item>
-      <el-form-item label="功能类型" prop="type">
+      label-width="68px">
+      <el-form-item label="功能类型" prop="name">
         <el-select
           v-model="queryParams.type"
-          class="!w-150px"
+          class="!w-240px"
           clearable
-          placeholder="请选择功能类型">
+          placeholder="请选择功能类型"
+          @change="handleQuery">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
             :key="dict.value"
@@ -36,121 +23,101 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
         <el-button plain type="primary" @click="openForm('create')">
           <Icon class="mr-5px" icon="ep:plus" />
-          新增
+          添加功能
+        </el-button>
+        <el-button v-hasPermi="[`iot:thing-model:query`]" plain type="success" @click="openTSL">
+          TSL
         </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
+  <!-- 列表 -->
   <ContentWrap>
-    <zm-table :loading="loading" :data="list">
-      <zm-table-column align="center" label="功能标识" prop="identifier" min-width="160" />
-      <zm-table-column align="center" label="功能名称" prop="name" min-width="160" />
-      <zm-table-column align="center" label="功能描述" prop="description" min-width="180" />
-      <zm-table-column align="center" label="功能类型" prop="type" width="120">
-        <template #default="{ row }">
-          <dict-tag :type="DICT_TYPE.IOT_THING_MODEL_TYPE" :value="row.type" />
-        </template>
-      </zm-table-column>
-      <zm-table-column align="left" label="属性 JSON" min-width="220">
-        <template #default="{ row }">
-          <JsonPreview :value="row.property" />
-        </template>
-      </zm-table-column>
-      <zm-table-column align="left" label="事件 JSON" min-width="220">
-        <template #default="{ row }">
-          <JsonPreview :value="row.event" />
-        </template>
-      </zm-table-column>
-      <zm-table-column align="left" label="服务 JSON" min-width="220">
-        <template #default="{ row }">
-          <JsonPreview :value="row.service" />
-        </template>
-      </zm-table-column>
-      <zm-table-column align="center" label="操作" fixed="right" width="140">
-        <template #default="{ row }">
-          <el-button link type="primary" @click="openForm('update', row.id)"> 编辑 </el-button>
-          <el-button link type="danger" @click="handleDelete(row.id)"> 删除 </el-button>
-        </template>
-      </zm-table-column>
-    </zm-table>
-    <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
-      :total="total"
-      @pagination="getList" />
+    <el-tabs>
+      <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+        <el-table-column align="center" label="功能类型" prop="type">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.IOT_THING_MODEL_TYPE" :value="scope.row.type" />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="功能名称" prop="name" />
+        <el-table-column align="center" label="标识符" prop="identifier" />
+        <el-table-column align="center" label="数据类型" prop="identifier">
+          <template #default="{ row }">
+            {{ getDataTypeOptionsLabel(row.property?.dataType) ?? '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column align="left" label="数据定义" prop="identifier">
+          <template #default="{ row }">
+            <DataDefinition :data="row" />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="操作">
+          <template #default="scope">
+            <el-button
+              v-hasPermi="[`iot:thing-model:update`]"
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)">
+              编辑
+            </el-button>
+            <el-button
+              v-hasPermi="['iot:thing-model:delete']"
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)">
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
+        @pagination="getList" />
+    </el-tabs>
   </ContentWrap>
 
-  <ThingModelForm ref="formRef" mode="common" @success="getList" />
+  <!-- 表单弹窗:添加/修改 -->
+  <ThingModelForm ref="formRef" @success="getList" />
+  <ThingModelTSL ref="tslRef" />
 </template>
-
-<script setup lang="ts">
-import { h } from 'vue'
-import { CommonThingModelData, ThingModelCommon } from '@/api/iot/thingmodel'
-import ThingModelForm from '@/views/iot/thingmodel/ThingModelFormCommon.vue'
+<script lang="ts" setup>
+import { ThingModelCommon, ThingModelData } from '@/api/iot/thingmodel'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import ThingModelForm from './ThingModelForm.vue'
+import ThingModelTSL from './ThingModelTSL.vue'
+import { ProductVO } from '@/api/iot/product/product'
+import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
+import { DataDefinition } from './components'
 
-defineOptions({ name: 'IoTCommonThingModel' })
-
-const { t } = useI18n()
-const message = useMessage()
+defineOptions({ name: 'IoTThingModel' })
 
-const loading = ref(false)
-const total = ref(0)
-const list = ref<CommonThingModelData[]>([])
-const queryFormRef = ref()
-const formRef = ref()
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
 
+const loading = ref(true) // 列表的加载中
+const list = ref<ThingModelData[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  identifier: '',
-  name: '',
-  type: undefined as number | undefined
+  type: undefined,
+  productId: -1
 })
 
-const formatJsonText = (value: unknown) => {
-  if (!value) {
-    return '-'
-  }
-  if (typeof value === 'string') {
-    return value
-  }
-  try {
-    return JSON.stringify(value, null, 2)
-  } catch {
-    return String(value)
-  }
-}
-
-const JsonPreview = (props: { value?: unknown }) =>
-  h(
-    'div',
-    {
-      style: {
-        whiteSpace: 'pre-wrap',
-        wordBreak: 'break-all',
-        lineHeight: '20px',
-        maxHeight: '120px',
-        overflow: 'auto'
-      }
-    },
-    formatJsonText(props.value)
-  )
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
 
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
+    queryParams.productId = product?.value?.id || -1
     const data = await ThingModelCommon.getCommonThingModelPage(queryParams)
     list.value = data.list
     total.value = data.total
@@ -159,32 +126,38 @@ const getList = async () => {
   }
 }
 
+/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-const resetQuery = () => {
-  queryFormRef.value?.resetFields()
-  handleQuery()
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
 }
 
-const openForm = (type: string, id?: number) => {
-  formRef.value?.open(type, id)
+/** 展示物模型 TSL */
+const tslRef = ref()
+const openTSL = () => {
+  tslRef.value?.open()
 }
 
-const handleDelete = async (id?: number) => {
-  if (!id) {
-    return
-  }
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
   try {
+    // 删除的二次确认
     await message.delConfirm()
+    // 发起删除
     await ThingModelCommon.deleteCommonThingModel(id)
     message.success(t('common.delSuccess'))
+    // 刷新列表
     await getList()
   } catch {}
 }
 
+/** 初始化 **/
 onMounted(() => {
   getList()
 })

+ 20 - 15
src/views/iot/thingmodel/ThingModelForm.vue

@@ -23,7 +23,7 @@
       <el-form-item label="标识符" prop="identifier">
         <el-input v-model="formData.identifier" placeholder="请输入标识符" />
       </el-form-item>
-      <!-- 属性配-->
+      <!-- 属性配�?-->
       <ThingModelProperty
         v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
         v-model="formData.property" />
@@ -46,8 +46,8 @@
     </el-form>
 
     <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确定</el-button>
+      <el-button @click="dialogVisible = false">取消</el-button>
     </template>
   </Dialog>
 </template>
@@ -67,17 +67,17 @@ import { cloneDeep } from 'lodash-es'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { isEmpty } from '@/utils/is'
 
-/** IoT 物模型数据表*/
+/** IoT 物模型数据表�?*/
 defineOptions({ name: 'IoTThingModelForm' })
 
-const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
+const product = inject<Ref<ProductVO> | undefined>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
 
-const { t } = useI18n() // 国际
+const { t } = useI18n() // 国际�?
 const message = useMessage() // 消息弹窗
 
-const dialogVisible = ref(false) // 弹窗的是否展
-const dialogTitle = ref('') // 弹窗的标
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const dialogVisible = ref(false) // 弹窗的是否展�?
+const dialogTitle = ref('') // 弹窗的标�?
+const formLoading = ref(false) // 表单的加载中�?)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref<ThingModelData>({
   type: IoTThingModelTypeEnum.PROPERTY,
@@ -114,11 +114,11 @@ const open = async (type: string, id?: number) => {
           }
         }
       }
-      // 情况二:服务初始
+      // 情况二:服务初始�?
       if (isEmpty(formData.value.service)) {
         formData.value.service = {}
       }
-      // 情况三:事件初始
+      // 情况三:事件初始�?
       if (isEmpty(formData.value.event)) {
         formData.value.event = {}
       }
@@ -137,8 +137,13 @@ const submitForm = async () => {
   try {
     const data = cloneDeep(formData.value) as ThingModelData
     // 信息补全
-    data.productId = product!.value.id
-    data.productKey = product!.value.productKey
+    if (product?.value?.id) {
+      data.productId = product.value.id
+      data.productKey = product.value.productKey
+    } else {
+      delete data.productId
+      delete data.productKey
+    }
     fillExtraAttributes(data)
     if (formType.value === 'create') {
       await ThingModelApi.createThingModel(data)
@@ -157,7 +162,7 @@ const submitForm = async () => {
 
 /** 填写额外的属性(处理不同类型的情况) */
 const fillExtraAttributes = (data: any) => {
-  // 属
+  // 属�?
   if (data.type === IoTThingModelTypeEnum.PROPERTY) {
     removeDataSpecs(data.property)
     data.dataType = data.property.dataType
@@ -186,7 +191,7 @@ const fillExtraAttributes = (data: any) => {
   }
 }
 
-/** 处理 dataSpecs 为空的情*/
+/** 处理 dataSpecs 为空的情�?*/
 const removeDataSpecs = (val: any) => {
   if (isEmpty(val.dataSpecs)) {
     delete val.dataSpecs

+ 20 - 3
src/views/pms/constructionDailyReport/components/ConstructionDailyReportFormSection.vue

@@ -39,6 +39,19 @@ function isFieldDisabled(field: ConstructionDailyReportField) {
 function getDictOptions(field: ConstructionDailyReportField) {
   return field.dictType ? getStrDictOptions(field.dictType) : []
 }
+
+function getNumberSuffix(field: ConstructionDailyReportField) {
+  return field.suffix || ''
+}
+
+function getGridColumnsClass() {
+  return props.section.columns === 3 ? 'grid-cols-3' : 'grid-cols-2'
+}
+
+function getFieldClass(field: ConstructionDailyReportField) {
+  if (field.span !== 2) return undefined
+  return props.section.columns === 3 ? 'col-span-3' : 'col-span-2'
+}
 </script>
 
 <template>
@@ -49,13 +62,13 @@ function getDictOptions(field: ConstructionDailyReportField) {
         {{ props.section.title }}
       </div>
     </div>
-    <div class="grid grid-cols-2 gap-4">
+    <div class="grid gap-4" :class="getGridColumnsClass()">
       <el-form-item
         v-for="field in props.section.fields"
         :key="`${props.section.title}-${String(field.prop)}-${field.label}`"
         :label="field.label"
         :prop="field.prop"
-        :class="{ 'col-span-2': field.span === 2 }"
+        :class="getFieldClass(field)"
       >
         <el-input
           v-if="field.type === 'textarea'"
@@ -88,7 +101,11 @@ function getDictOptions(field: ConstructionDailyReportField) {
           :precision="field.precision"
           :disabled="isFieldDisabled(field)"
           @update:model-value="updateField(field.prop, $event)"
-        />
+        >
+          <template v-if="getNumberSuffix(field)" #suffix>
+            <span class="text-[var(--el-text-color-secondary)]">{{ getNumberSuffix(field) }}</span>
+          </template>
+        </el-input-number>
         <el-input
           v-else
           size="default"

+ 40 - 14
src/views/pms/constructionDailyReport/components/ConstructionDailyReportTable.vue

@@ -1,7 +1,11 @@
 <script setup lang="ts">
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import dayjs from 'dayjs'
-import type { ConstructionDailyReportRow } from './report-config'
+import type {
+  ConstructionDailyReportRow,
+  ConstructionDailyReportTableColumn,
+  ConstructionDailyReportTableColumnLeaf
+} from './report-config'
 import { tableColumns } from './report-config'
 
 const props = withDefaults(
@@ -28,6 +32,12 @@ const emits = defineEmits<{
 
 const { ZmTable, ZmTableColumn } = useTableComponents<ConstructionDailyReportRow>()
 
+function isColumnGroup(
+  column: ConstructionDailyReportTableColumn
+): column is Extract<ConstructionDailyReportTableColumn, { children: ConstructionDailyReportTableColumnLeaf[] }> {
+  return 'children' in column
+}
+
 function formatCellValue(row: ConstructionDailyReportRow, prop: keyof ConstructionDailyReportRow) {
   const value = row[prop]
   if (prop === 'createTime' || prop === 'constructionStartDate' || prop === 'constructionEndDate') {
@@ -51,19 +61,35 @@ function formatCellValue(row: ConstructionDailyReportRow, prop: keyof Constructi
             :max-height="height"
             show-border>
             <ZmTableColumn type="index" label="序号" :width="60" fixed="left" />
-            <ZmTableColumn
-              v-for="column in tableColumns"
-              :key="`${String(column.prop)}-${column.label}`"
-              :prop="column.prop"
-              :label="column.label"
-              :width="column.width"
-              :min-width="column.minWidth"
-              :fixed="column.fixed"
-              show-overflow-tooltip
-              cover-formatter
-              :real-value="
-                (row: ConstructionDailyReportRow) => formatCellValue(row, column.prop)
-              " />
+            <template v-for="column in tableColumns" :key="column.label">
+              <ZmTableColumn v-if="isColumnGroup(column)" :label="column.label">
+                <ZmTableColumn
+                  v-for="child in column.children"
+                  :key="`${String(child.prop)}-${child.label}`"
+                  :prop="child.prop"
+                  :label="child.label"
+                  :width="child.width"
+                  :min-width="child.minWidth"
+                  :fixed="child.fixed"
+                  show-overflow-tooltip
+                  cover-formatter
+                  :real-value="
+                    (row: ConstructionDailyReportRow) => formatCellValue(row, child.prop)
+                  " />
+              </ZmTableColumn>
+              <ZmTableColumn
+                v-else
+                :prop="column.prop"
+                :label="column.label"
+                :width="column.width"
+                :min-width="column.minWidth"
+                :fixed="column.fixed"
+                show-overflow-tooltip
+                cover-formatter
+                :real-value="
+                  (row: ConstructionDailyReportRow) => formatCellValue(row, column.prop)
+                " />
+            </template>
             <ZmTableColumn label="操作" :width="120" fixed="right" action>
               <template #default="{ row }">
                 <slot name="action" :row="row"></slot>

+ 169 - 21
src/views/pms/constructionDailyReport/components/report-config.ts

@@ -22,6 +22,19 @@ export interface ConstructionDailyReportRow {
   cengshu?: number | null
   dailyFuel?: number | null
   nonProductionTime?: number | null
+  accidentTime?: number | null
+  repairTime?: number | null
+  selfStopTime?: number | null
+  complexityTime?: number | null
+  relocationTime?: number | null
+  rectificationTime?: number | null
+  waitingStopTime?: number | null
+  winterBreakTime?: number | null
+  partyaDesign?: number | null
+  partyaPrepare?: number | null
+  partyaResource?: number | null
+  otherNptTime?: number | null
+  otherNptReason?: string
   nptReason?: string
   singleWellWorkload?: number | string | null
   productionStatus?: string
@@ -42,6 +55,8 @@ export interface ConstructionDailyReportQuery {
   pageNo: number
   pageSize: number
   deptId?: number
+  projectDeptId?: number
+  teamId?: number
   projectName?: string
   wellName?: string
   createTime?: string[]
@@ -53,25 +68,37 @@ export interface ConstructionDailyReportField {
   type?: 'text' | 'number' | 'textarea' | 'select'
   span?: 1 | 2
   precision?: number
+  suffix?: string
   editableInEdit?: boolean
   dictType?: string
 }
 
 export interface ConstructionDailyReportSection {
   title: string
+  columns?: 2 | 3
   fields: ConstructionDailyReportField[]
 }
 
-export const tableColumns: Array<{
+export interface ConstructionDailyReportTableColumnLeaf {
   prop: keyof ConstructionDailyReportRow
   label: string
   width?: number
   minWidth?: number
   fixed?: 'left' | 'right'
-}> = [
+}
+
+export interface ConstructionDailyReportTableColumnGroup {
+  label: string
+  children: ConstructionDailyReportTableColumnLeaf[]
+}
+
+export type ConstructionDailyReportTableColumn =
+  | ConstructionDailyReportTableColumnLeaf
+  | ConstructionDailyReportTableColumnGroup
+
+export const tableColumns: ConstructionDailyReportTableColumn[] = [
   { prop: 'reportName', label: '日报名称', minWidth: 180, fixed: 'left' },
-  { prop: 'createTime', label: '创建(施工)日期', width: 120 },
-  { prop: 'constructionEndDate', label: '施工结束日期', width: 120 },
+  { prop: 'createTime', label: '创建日期', width: 120 },
   { prop: 'projectName', label: '项目', minWidth: 160, fixed: 'left' },
   { prop: 'location', label: '施工区域', minWidth: 140 },
   { prop: 'deptName', label: '队伍', minWidth: 140 },
@@ -81,12 +108,29 @@ export const tableColumns: Array<{
   { prop: 'fiveStatusLabel', label: '队伍状态', minWidth: 120 },
   { prop: 'dailyWorkingLayers', label: '当日主压层数', width: 120 },
   { prop: 'dailySandVolume', label: '当日加砂量(吨)', width: 130 },
-  { prop: 'dailyLiquidVolume', label: '当日注液量(m3)', width: 130 },
+  { prop: 'dailyLiquidVolume', label: '当日注液量()', width: 130 },
   { prop: 'monthWorkingWells', label: '当月作业井数', width: 120 },
   { prop: 'monthWorkingLayers', label: '当月作业层数', width: 120 },
   { prop: 'dailyFuel', label: '当日油耗(L)', width: 110 },
-  { prop: 'nonProductionTime', label: '当日非生产时间', width: 130 },
-  { prop: 'nptReason', label: '造成非生产时间原因', minWidth: 180 },
+  {
+    label: '非生产时间',
+    children: [
+      { prop: 'accidentTime', label: '工程质量', width: 110 },
+      { prop: 'repairTime', label: '设备故障', width: 110 },
+      { prop: 'selfStopTime', label: '设备保养', width: 110 },
+      { prop: 'complexityTime', label: '技术受限', width: 110 },
+      { prop: 'relocationTime', label: '生产配合', width: 110 },
+      { prop: 'rectificationTime', label: '生产组织', width: 110 },
+      { prop: 'waitingStopTime', label: '不可抗力', width: 110 },
+      { prop: 'winterBreakTime', label: '待命', width: 110 },
+      { prop: 'partyaDesign', label: '甲方设计', width: 110 },
+      { prop: 'partyaPrepare', label: '甲方准备', width: 110 },
+      { prop: 'partyaResource', label: '甲方资源', width: 110 },
+      { prop: 'otherNptTime', label: '其它非生产时间', width: 130 }
+    ]
+  },
+  { prop: 'otherNptReason', label: '其它非生产时间原因', minWidth: 180 },
+  // { prop: 'nptReason', label: '造成非生产时间原因', minWidth: 180 },
   { prop: 'singleWellWorkload', label: '单井工作量', width: 120 },
   { prop: 'productionStatus', label: '施工动态', minWidth: 180 },
   { prop: 'planWorkingLayers', label: '本月计划层数', width: 120 },
@@ -135,7 +179,7 @@ export const formSections: ConstructionDailyReportSection[] = [
       },
       {
         prop: 'dailyLiquidVolume',
-        label: '当日注液量()',
+        label: '当日注液量()',
         type: 'number',
         editableInEdit: true
       },
@@ -147,12 +191,6 @@ export const formSections: ConstructionDailyReportSection[] = [
         type: 'number',
         editableInEdit: true
       },
-      {
-        prop: 'nonProductionTime',
-        label: '当日非生产时间',
-        type: 'number',
-        editableInEdit: true
-      },
       { prop: 'singleWellWorkload', label: '单井工作量', editableInEdit: true },
       { prop: 'planWorkingLayers', label: '本月计划层数', type: 'number' }
     ]
@@ -160,13 +198,13 @@ export const formSections: ConstructionDailyReportSection[] = [
   {
     title: '现场说明',
     fields: [
-      {
-        prop: 'nptReason',
-        label: '造成非生产时间原因',
-        type: 'textarea',
-        span: 2,
-        editableInEdit: true
-      },
+      // {
+      //   prop: 'nptReason',
+      //   label: '造成非生产时间原因',
+      //   type: 'textarea',
+      //   span: 2,
+      //   editableInEdit: true
+      // },
       {
         prop: 'productionStatus',
         label: '施工动态',
@@ -177,6 +215,103 @@ export const formSections: ConstructionDailyReportSection[] = [
       { prop: 'weather', label: '天气', editableInEdit: true },
       { prop: 'temperature', label: '气温(℃)', editableInEdit: true }
     ]
+  },
+  {
+    title: '非生产时间',
+    columns: 3,
+    fields: [
+      {
+        prop: 'accidentTime',
+        label: '工程质量',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'repairTime',
+        label: '设备故障',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'selfStopTime',
+        label: '设备保养',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'complexityTime',
+        label: '技术受限',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'relocationTime',
+        label: '生产配合',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'rectificationTime',
+        label: '生产组织',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'waitingStopTime',
+        label: '不可抗力',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'winterBreakTime',
+        label: '待命',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'partyaDesign',
+        label: '甲方设计',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'partyaPrepare',
+        label: '甲方准备',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'partyaResource',
+        label: '甲方资源',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'otherNptTime',
+        label: '其它非生产时间',
+        type: 'number',
+        suffix: '小时(H)',
+        editableInEdit: true
+      },
+      {
+        prop: 'otherNptReason',
+        label: '其它非生产时间原因',
+        type: 'textarea',
+        span: 2,
+        editableInEdit: true
+      }
+    ]
   }
 ]
 
@@ -191,6 +326,19 @@ export function createEmptyConstructionDailyReport(): ConstructionDailyReportRow
     cengshu: null,
     dailyFuel: null,
     nonProductionTime: null,
+    accidentTime: null,
+    repairTime: null,
+    selfStopTime: null,
+    complexityTime: null,
+    relocationTime: null,
+    rectificationTime: null,
+    waitingStopTime: null,
+    winterBreakTime: null,
+    partyaDesign: null,
+    partyaPrepare: null,
+    partyaResource: null,
+    otherNptTime: null,
+    otherNptReason: '',
     singleWellWorkload: null,
     planWorkingLayers: null
   }

+ 53 - 6
src/views/pms/constructionDailyReport/index.vue

@@ -14,20 +14,58 @@ defineOptions({ name: 'ConstructionDailyReport' })
 
 const userStore = useUserStore()
 const deptId = userStore.getUser.deptId
+const route = useRoute()
+
+const parseQueryNumber = (value: unknown) => {
+  if (Array.isArray(value)) {
+    return parseQueryNumber(value[0])
+  }
+  if (value === undefined || value === null || value === '') {
+    return undefined
+  }
+  const parsedValue = Number(value)
+  return Number.isNaN(parsedValue) ? undefined : parsedValue
+}
 
-const initQuery: ConstructionDailyReportQuery = {
+const resolveCreateTimeFromRoute = () => {
+  const createTime = route.query.createTime
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item): item is string => typeof item === 'string')
+    return values.length === 2 ? values : undefined
+  }
+  if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    return values.length === 2 ? values : undefined
+  }
+  return undefined
+}
+
+const createDefaultQuery = (): ConstructionDailyReportQuery => ({
   pageNo: 1,
   pageSize: 10,
-  deptId
-}
+  deptId: parseQueryNumber(route.query.deptId) ?? deptId,
+  projectDeptId: parseQueryNumber(route.query.projectDeptId),
+  teamId: parseQueryNumber(route.query.teamId),
+  createTime: resolveCreateTimeFromRoute()
+})
 
-const query = ref<ConstructionDailyReportQuery>({ ...initQuery })
+const query = ref<ConstructionDailyReportQuery>(createDefaultQuery())
 const list = ref<ConstructionDailyReportRow[]>([])
 const total = ref(0)
 const loading = ref(false)
 const visible = ref(false)
 const formRef = ref<InstanceType<typeof ConstructionDailyReportDrawer>>()
-const route = useRoute()
+
+function syncQueryFromRoute() {
+  const nextQuery = createDefaultQuery()
+  query.value = {
+    ...query.value,
+    deptId: nextQuery.deptId,
+    projectDeptId: nextQuery.projectDeptId,
+    teamId: nextQuery.teamId,
+    createTime: nextQuery.createTime
+  }
+}
 
 const loadList = useDebounceFn(async () => {
   loading.value = true
@@ -48,7 +86,7 @@ function handleQuery(setPage = true) {
 }
 
 function resetQuery() {
-  query.value = { ...initQuery }
+  query.value = createDefaultQuery()
   handleQuery()
 }
 
@@ -69,6 +107,8 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
 watch(
   [
     () => query.value.deptId,
+    () => query.value.projectDeptId,
+    () => query.value.teamId,
     () => query.value.projectName,
     () => query.value.wellName,
     () => query.value.createTime
@@ -77,6 +117,13 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => [route.query.deptId, route.query.projectDeptId, route.query.teamId, route.query.createTime],
+  () => {
+    syncQueryFromRoute()
+  }
+)
+
 onMounted(() => {
   if (route.query.id) {
     handleOpenForm(Number(route.query.id), 'edit')

+ 307 - 168
src/views/pms/device/CustomerList.vue

@@ -1,176 +1,315 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 1100px;min-height: 800px">
-  <ContentWrap>
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label-width="90px" label="客商名称" prop="name" >
-        <el-input
-          @keyup.enter="handleQuery"
-          v-model="queryParams.name"
-          placeholder="请输入名称"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label-width="90px" label="客商编号" prop="code" >
-        <el-input
-          @keyup.enter="handleQuery"
-          v-model="queryParams.code"
-          placeholder="请输入名称"
-          class="!w-240px"
-        />
-      </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-form-item>
-    </el-form>
-  </ContentWrap>
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
-      <el-table-column width="60" label="选择">
-        <template #default="{ row }">
-          <el-radio
-            :model-value="selectedRow?.id"
-            :label="row.id"
-            @click.stop="selectRow(row)"
-            class="no-label-radio"
-          />
-        </template>
-      </el-table-column>
-      <el-table-column label="客商名称" align="center" prop="name" />
-      <el-table-column label="客商编号" align="center" prop="code" />
-      <el-table-column label="客商分类" align="center" prop="classification" >
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SUPPLIER_TYPE" :value="scope.row.classification" />
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" align="center" prop="status">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SUPPLIER_STATUS" :value="scope.row.status" />
-        </template>
-      </el-table-column>
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-<!--      <el-table-column label="操作" align="center">-->
-<!--        <template #default="scope">-->
-<!--          <el-button-->
-<!--            link-->
-<!--            type="primary"-->
-<!--            @click="choose(scope.row)"-->
-<!--          >-->
-<!--            选择-->
-<!--          </el-button>-->
-
-<!--        </template>-->
-<!--      </el-table-column>-->
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-    </Dialog>
+  <Dialog
+    v-model="dialogVisible"
+    width="min(1280px, calc(100vw - 32px))"
+    class="customer-select-dialog">
+    <template #title>
+      <div class="dialog-title">
+        <div class="dialog-title__icon"><Icon icon="ep:office-building" /></div>
+        <div>
+          <div class="dialog-title__text">选择客户</div>
+          <div class="dialog-title__subtitle">查询并选择需要关联的客户信息</div>
+        </div>
+      </div>
+    </template>
+
+    <div class="customer-select-content">
+      <el-form
+        ref="queryFormRef"
+        :model="queryParams"
+        label-width="76px"
+        size="default"
+        class="customer-query-form">
+        <div class="query-fields">
+          <el-form-item label="客商名称" prop="name">
+            <el-input
+              v-model="queryParams.name"
+              placeholder="请输入客商名称"
+              clearable
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="客商编号" prop="code">
+            <el-input
+              v-model="queryParams.code"
+              placeholder="请输入客商编号"
+              clearable
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item class="query-actions">
+            <el-button size="default" type="primary" @click="handleQuery">
+              <Icon icon="ep:search" class="mr-5px" />搜索
+            </el-button>
+            <el-button size="default" @click="resetQuery">
+              <Icon icon="ep:refresh" class="mr-5px" />重置
+            </el-button>
+          </el-form-item>
+        </div>
+      </el-form>
+
+      <section class="customer-table-section">
+        <div class="section-header">
+          <div class="section-title">客户列表</div>
+          <span class="section-total">共 {{ total }} 条</span>
+        </div>
+        <div class="table-container">
+          <ZmTable
+            :loading="loading"
+            :data="list"
+            height="360px"
+            show-border
+            @row-click="handleRowClick">
+            <ZmTableColumn width="70" label="选择" hide-in-column-settings>
+              <template #default="{ row }">
+                <el-radio
+                  :model-value="selectedRow?.id"
+                  :value="row.id"
+                  class="no-label-radio"
+                  @click.stop="selectRow(row)" />
+              </template>
+            </ZmTableColumn>
+            <ZmTableColumn label="客商名称" prop="name" />
+            <ZmTableColumn label="客商编号" prop="code" />
+            <ZmTableColumn label="客商分类" prop="classification">
+              <template #default="scope">
+                <dict-tag :type="DICT_TYPE.SUPPLIER_TYPE" :value="scope.row.classification" />
+              </template>
+            </ZmTableColumn>
+            <ZmTableColumn label="状态" prop="status">
+              <template #default="scope">
+                <dict-tag :type="DICT_TYPE.SUPPLIER_STATUS" :value="scope.row.status" />
+              </template>
+            </ZmTableColumn>
+            <ZmTableColumn label="创建时间" prop="createTime" :formatter="dateFormatter" />
+          </ZmTable>
+        </div>
+        <Pagination
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          :total="total"
+          @pagination="getList" />
+      </section>
+    </div>
+
+    <template #footer>
+      <el-button size="default" @click="dialogVisible = false">关闭</el-button>
+    </template>
+  </Dialog>
 </template>
 
 <script setup lang="ts">
-  import {DictDataVO} from "@/api/system/dict/dict.data";
-  import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
-  import {dateFormatter} from "@/utils/formatTime";
-  import {Api, SupplierVO} from '@/api/supplier/base'
-  import { propTypes } from '@/utils/propTypes'
-  const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
-  const dialogVisible = ref(false) // 弹窗的是否展示
-  const dialogTitle = ref('') // 弹窗的是否展示
-  const customerType = ref('') // 弹窗的是否展示
-  const loading = ref(true) // 列表的加载中
-  const queryFormRef = ref() // 搜索的表单
-  const list = ref<SupplierVO[]>([]) // 列表的数据
-  const total = ref(0) // 列表的总页数
-  // const props = defineProps({
-  //   type: propTypes.bool.def(), // 是否作为详情组件
-  // })
-  const selectedRow = ref(null);
-
-  // 处理单选逻辑
-  const selectRow = (row) => {
-    selectedRow.value = selectedRow.value?.id === row.id ? null : row;
-    emit('choose', row)
-    dialogVisible.value = false
-  };
-
-  // 点击整行选中
-  const handleRowClick = (row) => {
-    selectRow(row);
-  };
-  const queryParams = reactive({
-    pageNo: 1,
-    pageSize: 10,
-    customerNature: undefined,
-    code: undefined,
-    name: undefined,
-    classification: undefined,
-    type: undefined,
-    nature: undefined,
-    creditCode: undefined,
-    tin: undefined,
-    corporation: undefined,
-    incorporationDate: [],
-    address: undefined,
-    bizScope: undefined,
-    registeredCapital: undefined,
-    annualTurnove: undefined,
-    size: undefined,
-    status: undefined,
-    remark: undefined,
-    createTime: [],
-    customerNature: ''
-  })
-  const open = async (type: string) => {
-    dialogVisible.value = true
-    queryParams.name = undefined
-    selectedRow.value = null
-    await getList();
-  }
-  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-  const getList = async () => {
-    loading.value = true
-    try {
-      console.log(JSON.stringify(queryParams))
-      const data = await Api.getPage(queryParams)
-      list.value = data.list
-      total.value = data.total
-    } finally {
-      loading.value = false
-    }
-  }
-  /** 搜索按钮操作 */
-  const handleQuery = () => {
-    queryParams.pageNo = 1
-    getList()
-  }
-  const choose = (row: DictDataVO) => {
-    emit("choose", row);
-    dialogVisible.value = false;
-  }
-  /** 重置按钮操作 */
-  const resetQuery = () => {
-    queryFormRef.value.resetFields()
-    handleQuery()
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { Api, SupplierVO } from '@/api/supplier/base'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+const { ZmTable, ZmTableColumn } = useTableComponents()
+const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const queryFormRef = ref() // 搜索的表单
+const list = ref<SupplierVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+// const props = defineProps({
+//   type: propTypes.bool.def(), // 是否作为详情组件
+// })
+const selectedRow = ref<SupplierVO | null>(null)
+
+// 处理单选逻辑
+const selectRow = (row) => {
+  selectedRow.value = selectedRow.value?.id === row.id ? null : row
+  emit('choose', row)
+  dialogVisible.value = false
+}
+
+// 点击整行选中
+const handleRowClick = (row) => {
+  selectRow(row)
+}
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  customerNature: undefined,
+  code: undefined,
+  name: undefined,
+  classification: undefined,
+  type: undefined,
+  nature: undefined,
+  creditCode: undefined,
+  tin: undefined,
+  corporation: undefined,
+  incorporationDate: [],
+  address: undefined,
+  bizScope: undefined,
+  registeredCapital: undefined,
+  annualTurnove: undefined,
+  size: undefined,
+  status: undefined,
+  remark: undefined,
+  createTime: [],
+  customerNature: ''
+})
+const open = async () => {
+  dialogVisible.value = true
+  queryParams.name = undefined
+  selectedRow.value = null
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+const getList = async () => {
+  loading.value = true
+  try {
+    console.log(JSON.stringify(queryParams))
+    const data = await Api.getPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
   }
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
 </script>
-<style scoped lang="scss">
+<style scoped>
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.dialog-title__icon {
+  display: inline-flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 6px;
+}
+
+.dialog-title__text {
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.customer-select-content {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.customer-query-form {
+  padding: 14px 16px;
+  background: var(--el-fill-color-extra-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+}
+
+.query-fields {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(280px, 1fr)) auto;
+  gap: 12px 24px;
+  align-items: center;
+}
 
+.customer-query-form :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.query-actions {
+  min-width: max-content;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  gap: 10px;
+  flex-wrap: nowrap;
+  margin-left: 0 !important;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.customer-table-section {
+  min-width: 0;
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 44px;
+  padding: 0 14px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-title {
+  position: relative;
+  padding-left: 11px;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.section-title::before {
+  position: absolute;
+  top: 2px;
+  bottom: 2px;
+  left: 0;
+  width: 3px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+  content: '';
+}
+
+.section-total {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+.table-container {
+  min-width: 0;
+  padding: 12px 12px 0;
+}
+
+.customer-table-section :deep(.pagination-container) {
+  padding: 12px;
+}
+
+.no-label-radio :deep(.el-radio__label) {
+  display: none;
+}
+
+@media (width <= 768px) {
+  .dialog-title__subtitle {
+    display: none;
+  }
+
+  .query-fields {
+    grid-template-columns: minmax(0, 1fr);
+  }
+}
 </style>

+ 409 - 173
src/views/pms/failure/IotFailureReportForm.vue

@@ -1,162 +1,268 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible" style="width: 68em">
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="1080px" class="failure-report-dialog">
+    <template #title>
+      <div class="dialog-header mb-6">
+        <div class="dialog-header__icon">
+          <Icon icon="ep:warning" :size="22" />
+        </div>
+        <div>
+          <div class="dialog-title">{{ dialogTitle }}</div>
+          <div class="dialog-subtitle">维护故障基础信息、处理状态与现场描述</div>
+        </div>
+      </div>
+    </template>
+
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-row >
-        <el-col :span="12">
-          <el-form-item :label="t('faultForm.failureType')" prop="failureType">
-            <el-select v-model="formData.failureType" :placeholder="t('faultForm.failureTypeHolder')" :disabled="disabled" clearable>
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.FAILURE_TYPE)"
-                :key="dict.label"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-      <el-col :span="12">
-      <el-form-item :label="t('faultForm.device')" prop="deviceName">
-        <el-select
-          v-if="disabled"
-          :disabled="disabled"
-          v-model="formData.deviceName"
-          :placeholder="t('faultForm.choose')"
-          clearable
-          @clear="handleClear"
-        />
-        <el-select
-          v-if="!disabled"
-          v-model="formData.deviceName"
-          :placeholder="t('faultForm.choose')"
-          @click="openForm"
-          clearable
-          @clear="handleClear"
-        />
-      </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('faultForm.solve')" prop="ifDeal">
-            <el-select v-model="formData.ifDeal" :placeholder="t('faultForm.choose')" @change="dealChange" clearable :disabled="disabled">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.label"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('faultForm.assist')" prop="needHelp" >
-            <el-select v-model="formData.needHelp" :placeholder="t('faultForm.choose')" @change="helpChange" clearable :disabled="disabled">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.label"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('faultForm.faultySystem')" prop="failureSystem" :disabled="disabled">
-            <el-input v-model="formData.failureSystem" :placeholder="t('faultForm.faultySystemHolder')" :disabled="disabled" />
-          </el-form-item>
-        </el-col>
-      <el-col :span="12">
-      <el-form-item :label="t('faultForm.failureTime')" prop="failureTime" :disabled="disabled">
-        <el-date-picker
-          :disabled="disabled"
-          style="width: 150%"
-          v-model="formData.failureTime"
-          type="datetime"
-          value-format="x"
-          :placeholder="t('faultForm.failureTimeHolder')"
-        />
-      </el-form-item>
-        </el-col>
-        <el-col :span="12" v-if="formData.ifDeal">
-          <el-form-item :label="t('faultForm.dealTime')" prop="dealTime">
-            <el-date-picker
-              @change="endTimeBlur"
-              :disabled="disabled"
-              style="width: 150%"
-              v-model="formData.dealTime"
-              type="datetime"
-              value-format="x"
-              placeholder="选择故障解决时间"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('faultForm.shutDown')" prop="ifStop">
-            <el-select v-model="formData.ifStop" :placeholder="t('faultForm.choose')" clearable :disabled="disabled">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.label"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('faultForm.picture')" prop="pic">
-            <UploadImg v-model="formData.pic" height="60px" :disabled="disabled" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12" v-if="!formData.ifDeal&&approvalShow">
-          <el-form-item :label="t('faultForm.approvalId')" prop="approvalId" :rules="!formData.ifDeal?approvalRule:[]">
-            <el-select multiple v-model="formData.approvalId" :placeholder="t('faultForm.approvalHolder')" filterable clearable :disabled="disabled">
-              <el-option
-                v-for="ap in approver"
-                :key="ap.id"
-                :label="ap.nickname"
-                :value="ap.id"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="24">
-          <el-form-item :label="t('faultForm.faultImpact')" prop="failureInfluence">
-            <el-input type="textarea" v-model="formData.failureInfluence" :placeholder="t('faultForm.faultImpactHolder')" :disabled="disabled" />
-          </el-form-item>
-        </el-col>
-      <el-col :span="24">
-      <el-form-item label="解决办法" v-if="formData.ifDeal" prop="solution" :rules="formData.ifDeal?solutionRule:[]">
-        <el-input v-model="formData.solution" type="textarea" placeholder="请输入解决办法" :disabled="disabled" />
-      </el-form-item>
-        </el-col>
-        <el-col :span="24">
-          <el-form-item :label="t('faultForm.faultDescription')" prop="description">
-            <el-input type="textarea" v-model="formData.description" :disabled="disabled" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="24">
-      <el-form-item :label="t('faultForm.remark')" prop="remark">
-        <el-input v-model="formData.remark" type="textarea" :placeholder="t('faultForm.rHolder')" :disabled="disabled" />
-      </el-form-item>
-          </el-col>
-      </el-row>
+      size="default"
+      label-width="110px"
+      class="failure-report-form"
+      :class="{ 'is-readonly': disabled }">
+      <section class="info-card">
+        <div class="info-card__header">
+          <div class="info-card__title">
+            <span class="info-card__marker"></span>
+            <span>基础信息</span>
+          </div>
+        </div>
+        <div class="info-card__body">
+          <el-row :gutter="16">
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.failureType')" prop="failureType">
+                <el-select
+                  v-model="formData.failureType"
+                  :placeholder="t('faultForm.failureTypeHolder')"
+                  :disabled="disabled"
+                  clearable
+                  class="form-control">
+                  <el-option
+                    v-for="dict in getStrDictOptions(DICT_TYPE.FAILURE_TYPE)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.device')" prop="deviceName">
+                <el-select
+                  v-if="disabled"
+                  v-model="formData.deviceName"
+                  :disabled="disabled"
+                  :placeholder="t('faultForm.choose')"
+                  clearable
+                  class="form-control"
+                  @clear="handleClear" />
+                <el-select
+                  v-else
+                  v-model="formData.deviceName"
+                  :placeholder="t('faultForm.choose')"
+                  clearable
+                  class="form-control"
+                  @click="openForm"
+                  @clear="handleClear" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.faultySystem')" prop="failureSystem">
+                <el-input
+                  v-model="formData.failureSystem"
+                  :placeholder="t('faultForm.faultySystemHolder')"
+                  :disabled="disabled"
+                  class="form-control" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.failureTime')" prop="failureTime">
+                <el-date-picker
+                  v-model="formData.failureTime"
+                  :disabled="disabled"
+                  type="datetime"
+                  value-format="x"
+                  :placeholder="t('faultForm.failureTimeHolder')"
+                  class="form-control" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.shutDown')" prop="ifStop">
+                <el-select
+                  v-model="formData.ifStop"
+                  :placeholder="t('faultForm.choose')"
+                  clearable
+                  :disabled="disabled"
+                  class="form-control">
+                  <el-option
+                    v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.picture')" prop="pic">
+                <UploadImg v-model="formData.pic" height="60px" :disabled="disabled" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </section>
+
+      <section class="info-card">
+        <div class="info-card__header">
+          <div class="info-card__title">
+            <span class="info-card__marker"></span>
+            <span>处理信息</span>
+          </div>
+        </div>
+        <div class="info-card__body">
+          <el-row :gutter="16">
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.solve')" prop="ifDeal">
+                <el-select
+                  v-model="formData.ifDeal"
+                  :placeholder="t('faultForm.choose')"
+                  clearable
+                  :disabled="disabled"
+                  class="form-control"
+                  @change="dealChange">
+                  <el-option
+                    v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.assist')" prop="needHelp">
+                <el-select
+                  v-model="formData.needHelp"
+                  :placeholder="t('faultForm.choose')"
+                  clearable
+                  :disabled="disabled"
+                  class="form-control"
+                  @change="helpChange">
+                  <el-option
+                    v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col v-if="formData.ifDeal" :xs="24" :sm="12">
+              <el-form-item :label="t('faultForm.dealTime')" prop="dealTime">
+                <el-date-picker
+                  v-model="formData.dealTime"
+                  :disabled="disabled"
+                  type="datetime"
+                  value-format="x"
+                  placeholder="选择故障解决时间"
+                  class="form-control"
+                  @change="endTimeBlur" />
+              </el-form-item>
+            </el-col>
+            <el-col v-if="!formData.ifDeal && approvalShow" :xs="24" :sm="12">
+              <el-form-item
+                :label="t('faultForm.approvalId')"
+                prop="approvalId"
+                :rules="!formData.ifDeal ? approvalRule : []">
+                <el-select
+                  v-model="formData.approvalId"
+                  multiple
+                  :placeholder="t('faultForm.approvalHolder')"
+                  filterable
+                  clearable
+                  :disabled="disabled"
+                  class="form-control">
+                  <el-option
+                    v-for="ap in approver"
+                    :key="ap.id"
+                    :label="ap.nickname"
+                    :value="ap.id" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </section>
+
+      <section class="info-card">
+        <div class="info-card__header">
+          <div class="info-card__title">
+            <span class="info-card__marker"></span>
+            <span>故障描述</span>
+          </div>
+        </div>
+        <div class="info-card__body">
+          <el-row :gutter="16">
+            <el-col :span="24">
+              <el-form-item :label="t('faultForm.faultImpact')" prop="failureInfluence">
+                <el-input
+                  v-model="formData.failureInfluence"
+                  type="textarea"
+                  :rows="2"
+                  :placeholder="t('faultForm.faultImpactHolder')"
+                  :disabled="disabled" />
+              </el-form-item>
+            </el-col>
+            <el-col v-if="formData.ifDeal" :span="24">
+              <el-form-item
+                label="解决办法"
+                prop="solution"
+                :rules="formData.ifDeal ? solutionRule : []">
+                <el-input
+                  v-model="formData.solution"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="请输入解决办法"
+                  :disabled="disabled" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item :label="t('faultForm.faultDescription')" prop="description">
+                <el-input
+                  v-model="formData.description"
+                  type="textarea"
+                  :rows="3"
+                  :disabled="disabled" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item :label="t('faultForm.remark')" prop="remark">
+                <el-input
+                  v-model="formData.remark"
+                  type="textarea"
+                  :rows="2"
+                  :placeholder="t('faultForm.rHolder')"
+                  :disabled="disabled" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </section>
     </el-form>
+
     <template #footer>
-      <el-button @click="submitForm" type="primary" v-if="!disabled" :disabled="formLoading">{{t('faultForm.ok')}}</el-button>
-      <el-button @click="dialogVisible = false">{{t('faultForm.cancel')}}</el-button>
+      <div class="dialog-footer">
+        <el-button @click="dialogVisible = false">{{ t('faultForm.cancel') }}</el-button>
+        <el-button v-if="!disabled" type="primary" :disabled="formLoading" @click="submitForm">
+          {{ t('faultForm.ok') }}
+        </el-button>
+      </div>
     </template>
     <DeviceList ref="deviceFormRef" @choose="deviceChoose" />
   </Dialog>
 </template>
 <script setup lang="ts">
 import { IotFailureReportApi, IotFailureReportVO } from '@/api/pms/failure'
-import {DICT_TYPE, getBoolDictOptions, getStrDictOptions} from "@/utils/dict";
-import DeviceList from "@/views/pms/failure/DeviceList.vue";
-import dayjs from "dayjs";
+import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
+import DeviceList from '@/views/pms/failure/DeviceList.vue'
+import dayjs from 'dayjs'
 import { IotDeviceApi } from '@/api/pms/device'
 
 /** 故障上报 表单 */
@@ -170,12 +276,11 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const approvalShow = ref(true)
-const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const approver = ref([])
 let company = ''
 const formData = ref({
   failureType: undefined,
-  approvalId:undefined,
+  approvalId: undefined,
   id: undefined,
   failureCode: undefined,
   failureName: undefined,
@@ -194,13 +299,12 @@ const formData = ref({
   needHelp: undefined,
   solution: undefined,
   remark: undefined,
-  deptId: undefined,
+  deptId: undefined
 })
 const dealChange = () => {
-  console.log(formData.value.ifDeal)
   formData.value.needHelp = !formData.value.ifDeal
-  if (!formData.value.ifDeal){
-    IotFailureReportApi.getApproval().then(res=>{
+  if (!formData.value.ifDeal) {
+    IotFailureReportApi.getApproval().then((res) => {
       approver.value = res
     })
   }
@@ -212,10 +316,10 @@ const deviceChoose = async (row) => {
   formData.value.deviceId = row.id
   formData.value.deviceName = row.deviceName
   formData.value.deviceCode = row.deviceCode
-  formData.value.deptId = row.deptId;
+  formData.value.deptId = row.deptId
 
   company = await IotDeviceApi.getCompanyByDevice(row.id)
-  if ('rh'===company) {
+  if ('rh' === company) {
     approvalShow.value = false
   } else {
     approvalShow.value = true
@@ -227,14 +331,14 @@ const endTimeBlur = () => {
     message.error('故障解决时间不得早于故障时间')
     formData.value.dealTime = undefined
   }
-  const now = dayjs();
-  const target = dayjs(formData.value.dealTime);
+  const now = dayjs()
+  const target = dayjs(formData.value.dealTime)
   if (target.isAfter(now)) {
     message.error('故障解决时间不得晚于当前时间')
     formData.value.dealTime = undefined
   }
 }
-const handleClear = () =>{
+const handleClear = () => {
   formData.value.deviceId = undefined
   formData.value.deviceName = undefined
   formData.value.deptId = undefined
@@ -249,28 +353,24 @@ const formRules = ref({
   deviceName: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
   ifDeal: [{ required: true, message: '是否解决不能为空', trigger: 'blur' }],
   needHelp: [{ required: true, message: '是否协助不能为空', trigger: 'blur' }],
-  failureTime:[{ required: true, message: '故障时间不能为空', trigger: 'blur' }],
-  failureSystem:[{ required: true, message: '故障系统不能为空', trigger: 'blur' }],
-  dealTime:[{ required: true, message: '故障解决时间不能为空', trigger: 'blur' }],
+  failureTime: [{ required: true, message: '故障时间不能为空', trigger: 'blur' }],
+  failureSystem: [{ required: true, message: '故障系统不能为空', trigger: 'blur' }],
+  dealTime: [{ required: true, message: '故障解决时间不能为空', trigger: 'blur' }]
   // solution:[{required: ()=> formData.value.ifDeal==='true', message: '解决办法不能为空', trigger: 'blur'}],
 })
-const solutionRule = [
-  { required: true, message: '解决办法不能为空', trigger: 'blur' }
-];
-const approvalRule = [
-  { required: true, message: '审批人不能为空', trigger: 'blur' }
-];
+const solutionRule = [{ required: true, message: '解决办法不能为空', trigger: 'blur' }]
+const approvalRule = [{ required: true, message: '审批人不能为空', trigger: 'blur' }]
 const formRef = ref() // 表单 Ref
 const disabled = ref(false)
 /** 打开弹窗 */
-const open = async (type: string, id?: number, disable?:boolean) => {
+const open = async (type: string, id?: number, disable?: boolean) => {
   disabled.value = disable
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  formData.value.deviceId = null;
-  formData.value.deviceName = null;
+  formData.value.deviceId = null
+  formData.value.deviceName = null
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -293,9 +393,8 @@ const submitForm = async () => {
   try {
     // formData.value.pic = formData.value.pic.path;
     const data = formData.value as unknown as IotFailureReportVO
-    if (!formData.value.ifDeal){
-      if (company!=='rh')
-      formData.value.approvalId = formData.value.approvalId.join(',')
+    if (!formData.value.ifDeal) {
+      if (company !== 'rh') formData.value.approvalId = formData.value.approvalId.join(',')
     }
     if (formType.value === 'create') {
       await IotFailureReportApi.createIotFailureReport(data)
@@ -329,8 +428,145 @@ const resetForm = () => {
     ifDeal: undefined,
     needHelp: undefined,
     solution: undefined,
-    remark: undefined,
+    remark: undefined
   }
   formRef.value?.resetFields()
 }
 </script>
+
+<style lang="scss">
+.failure-report-dialog {
+  .el-dialog {
+    overflow: hidden;
+    border-radius: 8px;
+  }
+
+  .el-dialog__header {
+    min-height: 56px;
+    padding: 8px 18px;
+    margin-right: 0 !important;
+    background: var(--el-bg-color);
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+
+  .el-dialog__body {
+    padding: 14px 16px 0 !important;
+    background: var(--el-bg-color);
+  }
+
+  .el-dialog__footer {
+    padding: 10px 18px;
+    background: var(--el-bg-color);
+    border-top: 1px solid var(--el-border-color-lighter);
+  }
+}
+</style>
+
+<style scoped lang="scss">
+.dialog-header {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+
+.dialog-header__icon {
+  display: flex;
+  width: 36px;
+  height: 36px;
+  color: var(--el-color-primary);
+  background: rgb(64 158 255 / 12%);
+  border: 1px solid rgb(64 158 255 / 18%);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: center;
+}
+
+.dialog-title {
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-subtitle {
+  margin-top: 2px;
+  font-size: 12px;
+  line-height: 16px;
+  color: var(--el-text-color-secondary);
+}
+
+.failure-report-form {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin-bottom: 16px;
+}
+
+.info-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 6px 18px rgb(31 45 61 / 4%);
+}
+
+.info-card__header {
+  display: flex;
+  min-height: 40px;
+  padding: 0 16px;
+  background: linear-gradient(90deg, rgb(64 158 255 / 8%) 0%, rgb(255 255 255 / 0%) 72%);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  align-items: center;
+}
+
+.info-card__title {
+  display: flex;
+  gap: 8px;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.info-card__marker {
+  width: 4px;
+  height: 16px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.info-card__body {
+  padding: 12px 16px 0;
+}
+
+.form-control {
+  width: 100%;
+}
+
+.failure-report-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.failure-report-form :deep(.el-form-item__label) {
+  font-weight: 600;
+  color: var(--el-text-color-regular);
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+@media (width <= 768px) {
+  .info-card__header {
+    min-height: auto;
+    padding: 10px 14px;
+  }
+
+  .info-card__body {
+    padding: 12px 14px 0;
+  }
+}
+</style>

+ 398 - 289
src/views/pms/failure/index.vue

@@ -1,256 +1,256 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <!--          <el-form-item label="故障编码" prop="failureCode">-->
-          <!--            <el-input-->
-          <!--              v-model="queryParams.failureCode"-->
-          <!--              placeholder="请输入故障编码"-->
-          <!--              clearable-->
-          <!--              @keyup.enter="handleQuery"-->
-          <!--              class="!w-200px"-->
-          <!--            />-->
-          <!--          </el-form-item>-->
-          <el-form-item :label="t('fault.faultTitle')" label-width="70px" prop="failureName">
-            <el-input
-              v-model="queryParams.failureName"
-              :placeholder="t('fault.titleHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('fault.status')" label-width="40px" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              :placeholder="t('fault.status')"
-              clearable
-              class="!w-200px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FAILURE_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('fault.shutDown')" v-show="ifShow" prop="ifStop">
-            <el-select
-              v-model="queryParams.ifStop"
-              :placeholder="t('fault.shutDown')"
-              clearable
-              class="!w-200px"
-            >
-              <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="t('fault.failureTime')" v-show="ifShow" prop="failureTime">
-            <el-date-picker
-              v-model="queryParams.failureTime"
-              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-220px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('fault.solve')" v-show="ifShow" prop="ifDeal">
-            <el-select
-              v-model="queryParams.ifDeal"
-              :placeholder="t('fault.solve')"
-              clearable
-              class="!w-200px"
-            >
-              <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="t('fault.assist')" v-show="ifShow" prop="needHelp">
-            <el-select
-              v-model="queryParams.needHelp"
-              :placeholder="t('fault.assist')"
-              clearable
-              class="!w-200px"
-            >
-              <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="创建时间" v-show="ifShow" 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-220px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button v-if="!ifShow" @click="moreQuery(true)" type="warning"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('fault.moreSearch') }}</el-button
-            >
-            <el-button v-if="ifShow" @click="moreQuery(false)" type="danger"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('fault.closeSearch') }}</el-button
-            >
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('fault.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('fault.reset') }}</el-button
-            >
-            <el-button type="primary" plain @click="openForm('create', undefined, false)">
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('fault.added') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-failure-report:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="failure-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      :top-id="rootDeptId"
+      :dept-id="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="failure-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="failure-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('fault.faultTitle')" label-width="70px" prop="failureName">
+          <el-input
+            v-model="queryParams.failureName"
+            :placeholder="t('fault.titleHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('fault.status')" label-width="40px" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            :placeholder="t('fault.status')"
+            clearable
+            class="query-control query-control--small">
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FAILURE_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-show="ifShow" :label="t('fault.shutDown')" prop="ifStop">
+          <el-select
+            v-model="queryParams.ifStop"
+            :placeholder="t('fault.shutDown')"
+            clearable
+            class="query-control query-control--small">
+            <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 v-show="ifShow" :label="t('fault.failureTime')" prop="failureTime">
+          <el-date-picker
+            v-model="queryParams.failureTime"
+            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="query-control query-control--date" />
+        </el-form-item>
+        <el-form-item v-show="ifShow" :label="t('fault.solve')" prop="ifDeal">
+          <el-select
+            v-model="queryParams.ifDeal"
+            :placeholder="t('fault.solve')"
+            clearable
+            class="query-control query-control--small">
+            <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 v-show="ifShow" :label="t('fault.assist')" prop="needHelp">
+          <el-select
+            v-model="queryParams.needHelp"
+            :placeholder="t('fault.assist')"
+            clearable
+            class="query-control query-control--small">
+            <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 v-show="ifShow" 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="query-control query-control--date" />
+        </el-form-item>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table height="calc(85vh - 140px)" :loading="loading" :data="list" show-border>
-          <zm-table-column :label="t('fault.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <!--        <el-table-column label="故障编码" align="center" prop="failureCode" />-->
-          <zm-table-column :label="t('fault.faultTitle')" align="center" prop="failureName" />
-          <zm-table-column :label="t('iotMaintain.deviceCode')" align="center" prop="deviceCode" />
-          <zm-table-column :label="t('fault.deviceName')" align="center" prop="deviceName" />
-          <zm-table-column :label="t('fault.status')" align="center" prop="status">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_FAILURE_STATUS" :value="scope.row.status" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('faultForm.failureType')"
-            align="center"
-            prop="failureType"
-            min-width="90px"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.FAILURE_TYPE" :value="scope.row.failureType" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('fault.approvalStatus')"
-            align="center"
-            prop="auditStatus"
-            min-width="90px"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('fault.solve')" align="center" prop="ifDeal">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.ifDeal" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('fault.failureTime')"
-            align="center"
-            prop="failureTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column :label="t('fault.solveTime')" align="center" prop="dealHour">
-            <template #default="scope">
-              {{ scope.row.dealHour && scope.row.dealHour > 0 ? scope.row.dealHour + 'H' : '' }}
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('table.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('fault.operation')"
-            align="center"
-            min-width="120px"
-            fixed="right"
-            action
-          >
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('detail', scope.row.id, true)"
-                v-hasPermi="['rq:iot-failure-report:query']"
-              >
-                {{ t('fault.view') }}
-              </el-button>
-              <el-button
-                link
-                v-if="scope.row.auditStatus === '30'"
-                type="primary"
-                @click="openForm('update', scope.row.id, false)"
-                v-hasPermi="['rq:iot-failure-report:update']"
-              >
-                {{ t('fault.edit') }}
-              </el-button>
-              <el-button
-                link
-                v-if="scope.row.auditStatus === '30'"
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-failure-report:delete']"
-              >
-                {{ t('fault.del') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
-        <Pagination
+      <el-form-item class="query-actions">
+        <el-button v-if="!ifShow" type="warning" @click="moreQuery(true)">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('fault.moreSearch') }}
+        </el-button>
+        <el-button v-else type="danger" @click="moreQuery(false)">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('fault.closeSearch') }}
+        </el-button>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('fault.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> {{ t('fault.reset') }}
+        </el-button>
+        <el-button type="primary" plain @click="openForm('create', undefined, false)">
+          <Icon icon="ep:plus" class="mr-5px" /> {{ t('fault.added') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-failure-report:export']">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn :label="t('fault.serial')" width="70" align="center" fixed="left">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('fault.faultTitle')"
+                align="center"
+                prop="failureName"
+                fixed="left" />
+              <ZmTableColumn
+                :label="t('iotMaintain.deviceCode')"
+                align="center"
+                prop="deviceCode" />
+              <ZmTableColumn :label="t('fault.deviceName')" align="center" prop="deviceName" />
+              <ZmTableColumn :label="t('fault.status')" align="center" prop="status">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.PMS_FAILURE_STATUS" :value="scope.row.status" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('faultForm.failureType')"
+                align="center"
+                prop="failureType"
+                min-width="90px">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.FAILURE_TYPE" :value="scope.row.failureType" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('fault.approvalStatus')"
+                align="center"
+                prop="auditStatus"
+                min-width="90px">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('fault.solve')" align="center" prop="ifDeal">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.ifDeal" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('fault.failureTime')"
+                align="center"
+                prop="failureTime"
+                :formatter="dateFormatter"
+                min-width="180px" />
+              <ZmTableColumn :label="t('fault.solveTime')" align="center" prop="dealHour">
+                <template #default="scope">
+                  {{ scope.row.dealHour && scope.row.dealHour > 0 ? scope.row.dealHour + 'H' : '' }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('table.createTime')"
+                align="center"
+                prop="createTime"
+                :formatter="dateFormatter"
+                min-width="180px" />
+              <ZmTableColumn
+                :label="t('fault.operation')"
+                align="center"
+                min-width="120px"
+                fixed="right"
+                action>
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('detail', scope.row.id, true)"
+                    v-hasPermi="['rq:iot-failure-report:query']">
+                    {{ t('fault.view') }}
+                  </el-button>
+                  <el-button
+                    link
+                    v-if="scope.row.auditStatus === '30'"
+                    type="primary"
+                    @click="openForm('update', scope.row.id, false)"
+                    v-hasPermi="['rq:iot-failure-report:update']">
+                    {{ t('fault.edit') }}
+                  </el-button>
+                  <el-button
+                    link
+                    v-if="scope.row.auditStatus === '30'"
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                    v-hasPermi="['rq:iot-failure-report:delete']">
+                    {{ t('fault.del') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
   <!-- 表单弹窗:添加/修改 -->
   <IotFailureReportForm ref="formRef" @success="getList" />
 </template>
@@ -261,10 +261,11 @@ import download from '@/utils/download'
 import { IotFailureReportApi, IotFailureReportVO } from '@/api/pms/failure'
 import IotFailureReportForm from './IotFailureReportForm.vue'
 import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
+import { useUserStore } from '@/store/modules/user'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotFailureReportVO>()
 
 /** 故障上报 列表 */
 defineOptions({ name: 'IotFailureReport' })
@@ -275,7 +276,8 @@ const ifShow = ref(false)
 const loading = ref(true) // 列表的加载中
 const list = ref<IotFailureReportVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-let isLeftContentCollapsed = ref(false)
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -298,29 +300,9 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
-const moreQuery = (show) => {
+const moreQuery = (show: boolean) => {
   ifShow.value = show
 }
-const openWeb = (url) => {
-  window.open(
-    'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
-  )
-}
-const handleCommand = (command: string, row: IotFailureReportVO) => {
-  switch (command) {
-    case 'handleDelete':
-      handleDelete(row.id)
-      break
-    case 'handleUpdate':
-      openForm('update', row.id)
-      break
-    case 'handleSubmit':
-      submitProcess(row.id)
-      break
-    default:
-      break
-  }
-}
 
 /** 查询列表 */
 const getList = async () => {
@@ -334,8 +316,9 @@ const getList = async () => {
   }
 }
 /** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
+const handleDeptNodeClick = async (row: Tree) => {
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 /** 搜索按钮操作 */
@@ -346,10 +329,20 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number, disable?: boolean) => {
@@ -388,18 +381,134 @@ const handleExport = async () => {
 onMounted(() => {
   getList()
 })
-const submitProcess = async (row: IotFailureReportVO) => {
-  try {
-    // 提交审核的二次确认
-    await message.confirm(`您确定提交审核吗?`)
-    // 提交审核
-    loading.value = true
-    await IotFailureReportApi.submitForApproval(row).then((res) => {
-      loading.value = false
-    })
-    message.success('提交审核成功!')
-    // 刷新列表
-    await getList()
-  } catch {}
-}
 </script>
+
+<style scoped>
+.failure-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 24px;
+  min-width: 0;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 180px;
+}
+
+.query-control--small {
+  width: 140px;
+}
+
+.query-control--date {
+  width: 220px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2200px) {
+  .failure-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .failure-query {
+    gap: 12px 18px;
+  }
+
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 168px;
+  }
+
+  .query-control--small {
+    width: 132px;
+  }
+
+  .query-control--date {
+    width: 210px;
+  }
+}
+
+@media (width <= 1200px) {
+  .failure-page {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  :deep(.failure-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .failure-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--small,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+    margin-left: 0;
+  }
+}
+</style>

+ 493 - 0
src/views/pms/fiveDailyReportSummary/index.vue

@@ -0,0 +1,493 @@
+<script setup>
+import dayjs from 'dayjs'
+import CountTo from '@/components/count-to1.vue'
+import { FiveIotProjectDailyReportApi } from '@/api/pms/fiveconstructiondailyreport'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { useDebounceFn } from '@vueuse/core'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+defineOptions({ name: 'FiveDailyReportSummary' })
+
+const route = useRoute()
+const router = useRouter()
+const defaultDeptId = 388
+const { ZmTable, ZmTableColumn } = useTableComponents()
+
+const summaryCards = [
+  {
+    key: 'cumulativeWorkingLayers',
+    label: '压裂',
+    unit: '层',
+    icon: 'i-material-symbols-layers-outline-rounded',
+    decimals: 0
+  },
+  {
+    key: 'totalDailyFuel',
+    label: '油耗',
+    unit: 'L',
+    icon: 'i-material-symbols-local-gas-station-outline-rounded',
+    decimals: 0
+  },
+  {
+    key: 'cumulativeMixSand',
+    label: '加砂',
+    unit: '方',
+    icon: 'i-material-symbols-auto-mode-rounded',
+    decimals: 0
+  },
+  {
+    key: 'cumulativeLiquidVolume',
+    label: '注液',
+    unit: '方',
+    icon: 'i-material-symbols-water-drop-outline-rounded',
+    decimals: 0
+  },
+  {
+    key: 'utilizationRate',
+    label: '设备利用率',
+    unit: '%',
+    icon: 'i-material-symbols-build-outline-rounded',
+    decimals: 2
+  }
+]
+
+const tableColumns = [
+  { prop: 'projectDeptName', label: '部门', minWidth: 180 },
+  { prop: 'cumulativeLiquidVolume', label: '注液量(方)', minWidth: 140 },
+  { prop: 'cumulativeMixSand', label: '加砂(方)', minWidth: 140 },
+  { prop: 'cumulativeWorkingLayers', label: '压裂(层)', minWidth: 140 },
+  { prop: 'totalDailyFuel', label: '油耗(L)', minWidth: 140 },
+  { prop: 'utilizationRate', label: '设备利用率(%)', minWidth: 150 }
+]
+
+const createDefaultQuery = () => ({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: defaultDeptId,
+  createTime: [
+    ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+  ]
+})
+
+const query = ref(createDefaultQuery())
+const list = ref([])
+const total = ref(0)
+const loading = ref(false)
+const summaryLoading = ref(false)
+const currentDeptName = ref('5号国')
+const selectedDeptType = ref('1')
+const cardStatistics = ref({
+  cumulativeWorkingLayers: 0,
+  totalDailyFuel: 0,
+  cumulativeMixSand: 0,
+  cumulativeLiquidVolume: 0,
+  utilizationRate: 0
+})
+
+const deptColumnLabel = computed(() => (selectedDeptType.value === '1' ? '项目部' : '队伍'))
+
+const syncQueryFromRoute = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item) => typeof item === 'string')
+    if (values.length === 2) {
+      query.value.createTime = values
+    }
+  } else if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    if (values.length === 2) {
+      query.value.createTime = values
+    }
+  }
+}
+
+const loadList = useDebounceFn(async () => {
+  loading.value = true
+  try {
+    const data = await FiveIotProjectDailyReportApi.getProjectDailyReportSummaryPage({
+      pageNo: query.value.pageNo,
+      pageSize: query.value.pageSize,
+      deptId: query.value.deptId,
+      createTime: query.value.createTime
+    })
+    list.value = (data?.list ?? []).map((item) => ({
+      ...item,
+      deptDisplayName:
+        String(item.type || selectedDeptType.value || '1') === '2'
+          ? item.projectDeptName || '-'
+          : item.teamName || item.projectDeptName || '-'
+    }))
+    total.value = data?.total ?? 0
+  } finally {
+    loading.value = false
+  }
+}, 200)
+
+const loadSummary = useDebounceFn(async () => {
+  summaryLoading.value = true
+  try {
+    const data = await FiveIotProjectDailyReportApi.getIotProjectDailyReportSummary({
+      deptId: query.value.deptId,
+      createTime: query.value.createTime
+    })
+
+    cardStatistics.value = {
+      cumulativeWorkingLayers: data?.cumulativeWorkingLayers || 0,
+      totalDailyFuel: data?.totalDailyFuel || 0,
+      cumulativeMixSand: data?.cumulativeMixSand || 0,
+      cumulativeLiquidVolume: data?.cumulativeLiquidVolume || 0,
+      utilizationRate: Number(data?.utilizationRate || 0)
+    }
+  } finally {
+    summaryLoading.value = false
+  }
+}, 200)
+
+function handleQuery(resetPage = true) {
+  if (resetPage) {
+    query.value.pageNo = 1
+  }
+  loadList()
+}
+
+function resetQuery() {
+  query.value = createDefaultQuery()
+  currentDeptName.value = '5号国'
+  loadSummary()
+  handleQuery()
+}
+
+function handleDeptNodeClick(node) {
+  currentDeptName.value = node?.name || '5号国'
+  selectedDeptType.value = String(node?.type || '1')
+  loadSummary()
+  handleQuery()
+}
+
+function handleSizeChange(value) {
+  query.value.pageSize = value
+  handleQuery(false)
+}
+
+function handleCurrentChange(value) {
+  query.value.pageNo = value
+  loadList()
+}
+
+function formatCellValue(row, prop) {
+  if (prop === 'utilizationRate') {
+    return row[prop] === null || row[prop] === undefined ? '-' : `${Number(row[prop]).toFixed(2)}%`
+  }
+
+  const value = row[prop]
+  return value === null || value === undefined || value === '' ? '-' : value
+}
+
+function handleDeptCellClick(row) {
+  const rowType = String(row?.type || selectedDeptType.value || '1')
+  const targetQuery = {
+    deptId: String(query.value.deptId || defaultDeptId),
+    createTime: Array.isArray(query.value.createTime) ? query.value.createTime : undefined,
+    projectDeptId: undefined,
+    teamId: undefined
+  }
+
+  if (rowType === '2' && row?.projectDeptId) {
+    targetQuery.projectDeptId = String(row.projectDeptId)
+  } else if (row?.teamId) {
+    targetQuery.teamId = String(row.teamId)
+  } else if (row?.projectDeptId) {
+    targetQuery.projectDeptId = String(row.projectDeptId)
+  } else {
+    return
+  }
+
+  router.push({
+    name: 'ConstructionDailyReport',
+    query: targetQuery
+  })
+}
+
+syncQueryFromRoute()
+
+watch(
+  () => [query.value.createTime?.[0], query.value.createTime?.[1]],
+  () => {
+    loadSummary()
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+watch(
+  () => query.value.deptId,
+  () => {
+    loadSummary()
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+watch(
+  () => route.query.createTime,
+  () => {
+    syncQueryFromRoute()
+  }
+)
+</script>
+
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[48px_44px_1fr] gap-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      v-model="query.deptId"
+      :top-id="388"
+      :deptId="defaultDeptId"
+      :show-title="false"
+      class="row-span-3"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      size="small"
+      label-width="auto"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-3 pb-4 flex items-center flex-wrap min-w-0">
+      <div class="flex items-center gap-2 flex-wrap">
+        <el-form-item label="创建时间">
+          <el-date-picker
+            v-model="query.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-260px" />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @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-form-item>
+      </div>
+    </el-form>
+
+    <div class="summary-cards min-w-0" v-loading="summaryLoading">
+      <article
+        v-for="card in summaryCards"
+        :key="card.key"
+        class="summary-card bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-5 py-8 min-w-0">
+        <div class="summary-card__icon" :class="card.icon"></div>
+        <div class="min-w-0">
+          <div class="summary-card__label">{{ card.label }}({{ card.unit }})</div>
+          <count-to
+            class="summary-card__value"
+            :start-val="0"
+            :end-val="cardStatistics[card.key]"
+            :decimals="card.decimals || 0">
+            <span class="summary-card__placeholder">--</span>
+          </count-to>
+        </div>
+      </article>
+    </div>
+
+    <div class="summary-content min-h-0 min-w-0 overflow-hidden">
+      <div class="summary-content__header">
+        <h3 class="summary-content__title">{{ currentDeptName }}日报汇总</h3>
+      </div>
+
+      <div class="summary-table-wrap">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn type="index" label="序号" :width="70" fixed="left" />
+              <ZmTableColumn
+                prop="deptDisplayName"
+                :label="deptColumnLabel"
+                :min-width="180"
+                show-overflow-tooltip
+                cover-formatter
+                :real-value="(row) => formatCellValue(row, 'deptDisplayName')">
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    class="summary-table-link"
+                    @click.stop="handleDeptCellClick(row)">
+                    {{ formatCellValue(row, 'deptDisplayName') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                v-for="column in tableColumns.slice(1)"
+                :key="column.prop"
+                :prop="column.prop"
+                :label="column.label"
+                :min-width="column.minWidth"
+                show-overflow-tooltip
+                cover-formatter
+                :real-value="(row) => formatCellValue(row, column.prop)" />
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <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>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.summary-cards {
+  min-width: 0;
+  display: grid;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.summary-card {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  overflow: hidden;
+}
+
+/* .summary-card::after {
+  content: '';
+  position: absolute;
+  inset: auto -32px -32px auto;
+  width: 66px;
+  height: 66px;
+  border-radius: 999px;
+  background: linear-gradient(135deg, rgb(59 130 246 / 0.14), rgb(16 185 129 / 0.08));
+} */
+
+.summary-card__icon {
+  position: relative;
+  z-index: 1;
+  flex: none;
+  width: 44px;
+  height: 44px;
+  border-radius: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  color: #2563eb;
+  background: linear-gradient(135deg, rgb(37 99 235 / 0.14), rgb(14 165 233 / 0.08));
+}
+
+.summary-card__label {
+  position: relative;
+  z-index: 1;
+  color: var(--el-text-color-regular);
+  font-size: 13px;
+  line-height: 20px;
+  margin-bottom: 4px;
+}
+
+.summary-card__value {
+  position: relative;
+  z-index: 1;
+  font-size: 28px;
+  font-weight: 600;
+  line-height: 1;
+  color: var(--el-text-color-primary);
+}
+
+.summary-card__placeholder {
+  font-size: 16px;
+  color: var(--el-text-color-placeholder);
+}
+
+.summary-content {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: var(--el-box-shadow-light);
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-top: 20px;
+}
+
+.summary-content__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.summary-content__title {
+  font-size: 18px;
+  line-height: 28px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.summary-table-wrap {
+  position: relative;
+  flex: 1;
+  min-height: 0;
+}
+
+.summary-table-link {
+  padding: 0;
+  font-weight: 500;
+}
+
+@media (max-width: 1600px) {
+  .summary-card__value {
+    font-size: 24px;
+  }
+}
+
+@media (max-width: 1280px) {
+  .summary-card {
+    gap: 10px;
+    padding: 12px 14px;
+  }
+
+  .summary-card__icon {
+    width: 38px;
+    height: 38px;
+    border-radius: 12px;
+    font-size: 20px;
+  }
+
+  .summary-card__label {
+    font-size: 12px;
+    line-height: 18px;
+  }
+
+  .summary-card__value {
+    font-size: 20px;
+  }
+}
+</style>

+ 189 - 45
src/views/pms/information/IotInformationDbForm.vue

@@ -1,63 +1,100 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog
+    v-model="dialogVisible"
+    :title="dialogTitle"
+    width="760px"
+    :fullscreen="false"
+    class="information-form-dialog">
+    <template #title>
+      <div class="dialog-title mb-4">
+        <div class="dialog-title__icon">
+          <Icon icon="ep:notebook" />
+        </div>
+        <div>
+          <div class="dialog-title__text">{{ dialogTitle }}</div>
+          <div class="dialog-title__subtitle">维护设备故障现象、影响及解决方案</div>
+        </div>
+      </div>
+    </template>
+
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-row>
-        <el-col :span="12">
-          <el-form-item :label="t('info.deviceClass')" prop="deviceType" >
-            <el-tree-select
-              v-model="formData.deviceType"
-              :data="productClassifyList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              :placeholder="t('deviceForm.category')"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-        <el-form-item :label="t('faultForm.faultImpact')" prop="failureInfluence">
-          <el-input v-model="formData.failureInfluence" :placeholder="t('faultForm.faultImpactHolder')" />
-        </el-form-item>
+      size="default"
+      label-width="92px"
+      class="information-form"
+      v-loading="formLoading">
+      <section class="form-section">
+        <div class="section-heading"><span></span>基础信息</div>
+        <el-row :gutter="20">
+          <el-col :xs="24" :sm="12">
+            <el-form-item :label="t('info.deviceClass')" prop="deviceType">
+              <el-tree-select
+                v-model="formData.deviceType"
+                :data="productClassifyList"
+                :props="defaultProps"
+                check-strictly
+                node-key="id"
+                :placeholder="t('deviceForm.category')"
+                filterable
+                clearable />
+            </el-form-item>
           </el-col>
-        <el-col :span="12">
-        <el-form-item :label="t('faultForm.faultySystem')" prop="failureSystem">
-          <el-input v-model="formData.failureSystem" :placeholder="t('faultForm.faultySystemHolder')" />
-        </el-form-item>
+          <el-col :xs="24" :sm="12">
+            <el-form-item :label="t('faultForm.faultImpact')" prop="failureInfluence">
+              <el-input
+                v-model="formData.failureInfluence"
+                :placeholder="t('faultForm.faultImpactHolder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12">
+            <el-form-item :label="t('faultForm.faultySystem')" prop="failureSystem">
+              <el-input
+                v-model="formData.failureSystem"
+                :placeholder="t('faultForm.faultySystemHolder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12">
+            <el-form-item :label="t('iotMaintain.solution')" prop="solutions">
+              <el-input v-model="formData.solutions" :placeholder="t('iotMaintain.soHolder')" />
+            </el-form-item>
           </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('iotMaintain.solution')" prop="solutions">
-            <el-input v-model="formData.solutions" :placeholder="t('iotMaintain.soHolder')" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="24">
+        </el-row>
+      </section>
+
+      <section class="form-section">
+        <div class="section-heading"><span></span>补充说明</div>
         <el-form-item :label="t('info.description')" prop="description">
-          <el-input v-model="formData.description" type="textarea" height="150px" />
+          <el-input v-model="formData.description" type="textarea" :rows="3" resize="none" />
         </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" :placeholder="t('iotMaintain.remarkHolder')" />
+          <el-input
+            v-model="formData.remark"
+            type="textarea"
+            :rows="2"
+            resize="none"
+            :placeholder="t('iotMaintain.remarkHolder')" />
         </el-form-item>
-          </el-col>
-      </el-row>
+      </section>
     </el-form>
+
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('iotMaintain.ok') }}</el-button>
-      <el-button @click="dialogVisible = false">{{ t('iotMaintain.cancel') }}</el-button>
+      <div class="dialog-footer">
+        <el-button size="default" @click="dialogVisible = false">
+          {{ t('iotMaintain.cancel') }}
+        </el-button>
+        <el-button size="default" type="primary" :loading="formLoading" @click="submitForm">
+          {{ t('iotMaintain.ok') }}
+        </el-button>
+      </div>
     </template>
   </Dialog>
 </template>
 <script setup lang="ts">
 import { IotInformationDbApi, IotInformationDbVO } from '@/api/pms/information'
-import {defaultProps, handleTree} from "@/utils/tree";
-import * as ProductClassifyApi from "@/api/pms/productclassify";
-import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as ProductClassifyApi from '@/api/pms/productclassify'
 
 /** 故障知识库 表单 */
 defineOptions({ name: 'IotInformationDbForm' })
@@ -79,12 +116,12 @@ const formData = ref({
   solutions: undefined,
   remark: undefined,
   processInstanceId: undefined,
-  auditStatus: undefined,
+  auditStatus: undefined
 })
 const formRules = reactive({
   deviceType: [{ required: true, message: t('info.classHolder1'), trigger: 'change' }],
   failureInfluence: [{ required: true, message: t('info.faultyImpact'), trigger: 'change' }],
-  solutions: [{ required: true, message: t('info.solution'), trigger: 'change' }],
+  solutions: [{ required: true, message: t('info.solution'), trigger: 'change' }]
 })
 const formRef = ref() // 表单 Ref
 
@@ -144,8 +181,115 @@ const resetForm = () => {
     solutions: undefined,
     remark: undefined,
     processInstanceId: undefined,
-    auditStatus: undefined,
+    auditStatus: undefined
   }
   formRef.value?.resetFields()
 }
 </script>
+
+<style scoped>
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.dialog-title__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.information-form {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.form-section {
+  padding: 0 16px 2px;
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 16px;
+  margin: 0 -16px 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-heading span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.information-form :deep(.el-select),
+.information-form :deep(.el-tree-select) {
+  width: 100%;
+}
+
+.information-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+.dialog-footer :deep(.el-button) {
+  margin-left: 0;
+}
+
+@media (width <= 768px) {
+  .dialog-title__subtitle {
+    display: none;
+  }
+
+  .form-section {
+    padding-right: 12px;
+    padding-left: 12px;
+  }
+
+  .section-heading {
+    padding-right: 12px;
+    padding-left: 12px;
+    margin-right: -12px;
+    margin-left: -12px;
+  }
+}
+</style>

+ 220 - 113
src/views/pms/information/index.vue

@@ -1,133 +1,135 @@
 <template>
   <div
-    style="border: none; background: #fff; display: flex; align-items: center"
-    class="py-2 rounded-sm px-2"
-  >
-    <!-- 搜索工作栏 -->
+    class="information-page grid grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
-      :inline="true"
+      :model="queryParams"
+      size="default"
       label-width="68px"
-    >
-      <el-form-item :label="t('deviceForm.category')" prop="deviceType" style="width: 15vw">
-        <el-tree-select
-          v-model="queryParams.deviceType"
-          :data="productClassifyList"
-          :props="defaultProps"
-          check-strictly
-          node-key="id"
-          :placeholder="t('deviceForm.categoryHolder')"
-          filterable
-        />
-      </el-form-item>
-      <el-form-item :label="t('info.faultySystem')" prop="failureSystem">
-        <el-input
-          v-model="queryParams.failureSystem"
-          :placeholder="t('info.faultySystemHolder')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('info.createTime')" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          :start-placeholder="t('info.start')"
-          :end-placeholder="t('info.end')"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-220px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"
-          ><Icon icon="ep:search" class="mr-5px" /> {{ t('info.search') }}</el-button
-        >
-        <el-button @click="resetQuery"
-          ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('info.reset') }}</el-button
-        >
+      class="information-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('deviceForm.category')" prop="deviceType">
+          <el-tree-select
+            v-model="queryParams.deviceType"
+            :data="productClassifyList"
+            :props="defaultProps"
+            check-strictly
+            node-key="id"
+            :placeholder="t('deviceForm.categoryHolder')"
+            filterable
+            clearable
+            class="query-control query-control--category" />
+        </el-form-item>
+        <el-form-item :label="t('info.faultySystem')" prop="failureSystem">
+          <el-input
+            v-model="queryParams.failureSystem"
+            :placeholder="t('info.faultySystemHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('info.createTime')" prop="createTime">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            :start-placeholder="t('info.start')"
+            :end-placeholder="t('info.end')"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="query-control query-control--date" />
+        </el-form-item>
+      </div>
+
+      <el-form-item class="query-actions">
+        <el-button size="default" type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('info.search') }}
+        </el-button>
+        <el-button size="default" @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('info.reset') }}
+        </el-button>
         <el-button
+          size="default"
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['rq:iot-information-db:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" />
-          {{ t('info.add') }}
+          v-hasPermi="['rq:iot-information-db:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('info.add') }}
         </el-button>
         <el-button
+          size="default"
           type="success"
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['rq:iot-information-db:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          v-hasPermi="['rq:iot-information-db:export']">
+          <Icon icon="ep:download" class="mr-5px" />导出
         </el-button>
       </el-form-item>
     </el-form>
-  </div>
 
-  <!-- 列表 -->
-  <ContentWrap style="border: none; margin-top: 20px">
-    <zm-table :loading="loading" :data="list" height="70vh">
-      <zm-table-column :label="t('iotDevice.serial')" width="70" align="center">
-        <template #default="scope">
-          {{ scope.$index + 1 }}
-        </template>
-      </zm-table-column>
-      <zm-table-column :label="t('info.deviceClass')" align="center" prop="className" />
-      <zm-table-column :label="t('faultForm.faultImpact')" align="center" prop="failureInfluence" />
-      <zm-table-column :label="t('iotMaintain.faultySystem')" align="center" prop="failureSystem" />
-      <zm-table-column :label="t('info.description')" align="center" prop="description" />
-      <zm-table-column :label="t('iotMaintain.solution')" align="center" prop="solutions" />
-      <zm-table-column :label="t('iotMaintain.remark')" align="center" prop="remark" />
-      <zm-table-column
-        :label="t('info.createTime')"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <!--      <zm-table-column label="审核状态" align="center" prop="auditStatus" />-->
-      <zm-table-column
-        :label="t('iotMaintain.operation')"
-        align="center"
-        min-width="120px"
-        fixed="right"
-        action
-      >
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['rq:iot-information-db:update']"
-          >
-            {{ t('info.edit') }}
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['rq:iot-information-db:delete']"
-          >
-            {{ t('info.delete') }}
-          </el-button>
-        </template>
-      </zm-table-column>
-    </zm-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn type="index" :label="t('iotDevice.serial')" width="70" fixed="left" />
+              <ZmTableColumn :label="t('info.deviceClass')" prop="className" fixed="left" />
+              <ZmTableColumn :label="t('faultForm.faultImpact')" prop="failureInfluence" />
+              <ZmTableColumn :label="t('iotMaintain.faultySystem')" prop="failureSystem" />
+              <ZmTableColumn :label="t('info.description')" prop="description" />
+              <ZmTableColumn :label="t('iotMaintain.solution')" prop="solutions" />
+              <ZmTableColumn :label="t('iotMaintain.remark')" prop="remark" />
+              <ZmTableColumn
+                :label="t('info.createTime')"
+                prop="createTime"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn :label="t('iotMaintain.operation')" width="120" fixed="right" action>
+                <template #default="scope">
+                  <el-button
+                    size="default"
+                    link
+                    type="primary"
+                    @click="openForm('update', scope.row.id)"
+                    v-hasPermi="['rq:iot-information-db:update']">
+                    {{ t('info.edit') }}
+                  </el-button>
+                  <el-button
+                    size="default"
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                    v-hasPermi="['rq:iot-information-db:delete']">
+                    {{ t('info.delete') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.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>
+    </div>
+  </div>
 
   <!-- 表单弹窗:添加/修改 -->
   <IotInformationDbForm ref="formRef" style="width: 60vw" @success="getList" />
@@ -142,7 +144,7 @@ import { defaultProps, handleTree } from '@/utils/tree'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotInformationDbVO>()
 
 /** 故障知识库 列表 */
 defineOptions({ name: 'IotInformationDb' })
@@ -190,10 +192,20 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
   handleQuery()
 }
 
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
@@ -236,3 +248,98 @@ onMounted(async () => {
   await getList()
 })
 </script>
+
+<style scoped>
+.information-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 24px;
+  min-width: 0;
+}
+
+.query-row :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 200px;
+}
+
+.query-control--category {
+  width: 220px;
+}
+
+.query-control--date {
+  width: 240px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width <= 1200px) {
+  .information-page {
+    grid-template-rows: auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .information-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--category,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+  }
+}
+</style>

+ 206 - 69
src/views/pms/inspect/item/IotInspectItemForm.vue

@@ -1,78 +1,98 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible" style="width: 50vw;min-height: 50vh" >
+  <Dialog
+    v-model="dialogVisible"
+    :title="dialogTitle"
+    width="min(920px, 92vw)"
+    class="inspect-item-form-dialog">
+    <template #title>
+      <div class="dialog-title mb-4">
+        <div class="dialog-title__icon"><Icon icon="ep:list" /></div>
+        <div class="dialog-title__content">
+          <div class="dialog-title__text">{{ dialogTitle }}</div>
+          <div class="dialog-title__subtitle">维护巡检设备、巡检项目与执行标准</div>
+        </div>
+      </div>
+    </template>
+
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
+      size="default"
       label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-row>
-        <el-col :span="12">
-        <el-form-item :label="t('monitor.category')"  prop="deviceClassify">
-          <el-tree-select
-            filterable
-            v-model="formData.deviceClassify"
-            :data="productClassifyList"
-            :props="defaultProps"
-            check-strictly
-            node-key="id"
-            :placeholder="t('deviceForm.categoryHolder')"
-            clearable
-          />
-        </el-form-item>
-          </el-col>
-        <el-col :span="12">
+      class="inspect-item-form">
+      <section class="form-section">
+        <header class="section-heading"><span></span>基础信息</header>
+        <div class="form-grid">
+          <el-form-item :label="t('monitor.category')" prop="deviceClassify">
+            <el-tree-select
+              v-model="formData.deviceClassify"
+              :data="productClassifyList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              :placeholder="t('deviceForm.categoryHolder')"
+              filterable
+              clearable />
+          </el-form-item>
           <el-form-item :label="t('iotDevice.brand')" prop="brandName">
             <el-select
-              clearable
               v-model="formData.brandName"
-              @clear="brandClear"
               :placeholder="t('iotDevice.brandHolder')"
-              @click="openBrand"
-            />
+              clearable
+              @clear="brandClear"
+              @click="openBrand" />
+          </el-form-item>
+          <el-form-item :label="t('monitor.deviceName')" prop="deviceId">
+            <el-select
+              v-model="formData.deviceId"
+              :model-value="deviceLabel"
+              :placeholder="t('monitor.nameHolder')"
+              @click="openForm" />
           </el-form-item>
-        </el-col>
-        <el-col :span="12">
-        <el-form-item :label="t('monitor.deviceName')" prop="deviceId">
-          <el-select
-            v-model="formData.deviceId"
-            :model-value="deviceLabel"
-            :placeholder="t('monitor.nameHolder')"
-            @click="openForm"
-          />
-        </el-form-item>
-          </el-col>
-        <el-col :span="12">
-        <el-form-item :label="t('inspect.InspectionItems')" prop="item">
-          <el-input v-model="formData.item" :placeholder="t('inspect.itemHolder')" />
-        </el-form-item>
-          </el-col>
-        <el-col :span="12">
-        <el-form-item :label="t('common.sort')" prop="sort">
-          <el-input v-model="formData.sort" type="number" :placeholder="t('common.sortHolder')" />
-        </el-form-item>
-          </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('fileInfo.appendix')" prop="urls">
-            <UploadFile v-model="formData.urls" :limit="1" :is-show-tip="false" class="min-w-80px" />
+          <el-form-item :label="t('common.sort')" prop="sort">
+            <el-input v-model="formData.sort" type="number" :placeholder="t('common.sortHolder')" />
+          </el-form-item>
+          <el-form-item :label="t('fileInfo.appendix')" prop="urls" class="form-grid__full">
+            <UploadFile
+              v-model="formData.urls"
+              :limit="1"
+              :is-show-tip="false"
+              class="min-w-80px" />
+          </el-form-item>
+        </div>
+      </section>
+
+      <section class="form-section">
+        <header class="section-heading"><span></span>巡检内容</header>
+        <div class="form-content">
+          <el-form-item :label="t('inspect.InspectionItems')" prop="item">
+            <el-input v-model="formData.item" :placeholder="t('inspect.itemHolder')" />
           </el-form-item>
-        </el-col>
-        <el-col :span="24">
           <el-form-item :label="t('inspect.InspectionStandards')" prop="standard">
-            <el-input v-model="formData.standard" type="textarea" :placeholder="t('inspect.stanHolder')" />
+            <el-input
+              v-model="formData.standard"
+              type="textarea"
+              :rows="3"
+              :placeholder="t('inspect.stanHolder')" />
           </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')" />
-        </el-form-item>
-          </el-col>
-      </el-row>
+          <el-form-item :label="t('form.remark')" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              :rows="3"
+              :placeholder="t('deviceForm.remarkHolder')" />
+          </el-form-item>
+        </div>
+      </section>
     </el-form>
+
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('faultForm.ok') }}</el-button>
       <el-button @click="dialogVisible = false">{{ t('faultForm.cancel') }}</el-button>
+      <el-button type="primary" :loading="formLoading" @click="submitForm">
+        {{ t('faultForm.ok') }}
+      </el-button>
     </template>
   </Dialog>
   <DeviceList ref="deviceFormRef" @choose="deviceChoose" />
@@ -80,11 +100,11 @@
 </template>
 <script setup lang="ts">
 import { IotInspectItemApi, IotInspectItemVO } from '@/api/pms/inspect/item'
-import {defaultProps, handleTree} from "@/utils/tree";
-import * as ProductClassifyApi from "@/api/pms/productclassify";
-import DeviceList from "@/views/pms/failure/DeviceList.vue";
-import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
-import BrandList from "@/views/pms/device/BrandList.vue";
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as ProductClassifyApi from '@/api/pms/productclassify'
+import DeviceList from '@/views/pms/failure/DeviceList.vue'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import BrandList from '@/views/pms/device/BrandList.vue'
 /** 巡检项 表单 */
 defineOptions({ name: 'IotInspectItemForm' })
 
@@ -111,13 +131,13 @@ const formData = ref({
   deviceCode: undefined,
   urls: undefined,
   brand: undefined,
-  brandName: undefined,
+  brandName: undefined
 })
 const formRules = reactive({
   deviceClassify: [{ required: true, message: '设备类别不能为空', trigger: 'blur' }],
   item: [{ required: true, message: '巡检项不能为空', trigger: 'blur' }],
   standard: [{ required: true, message: '巡检标准不能为空', trigger: 'blur' }],
-  sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
@@ -125,7 +145,7 @@ const deviceChoose = (row) => {
   formData.value.deviceId = row.id
   formData.value.deviceName = row.deviceName
   formData.value.deviceCode = row.deviceCode
-  formData.value.deptId = row.deptId;
+  formData.value.deptId = row.deptId
   deviceLabel.value = row.deviceName
 }
 
@@ -138,7 +158,7 @@ const brandChoose = (row) => {
   // brandLabel.value = row.value
   formData.value.brandName = row.label
 }
-const brandClear = () =>{
+const brandClear = () => {
   formData.value.brand = undefined
   formData.value.brandName = undefined
 }
@@ -157,7 +177,7 @@ const open = async (type: string, id?: number) => {
     } finally {
       formLoading.value = false
     }
-  }else {
+  } else {
     formData.value.sort = 0
     deviceLabel.value = ''
   }
@@ -208,8 +228,125 @@ const resetForm = () => {
     standard: undefined,
     sort: undefined,
     remark: undefined,
-    deptId: undefined,
+    deptId: undefined
   }
   formRef.value?.resetFields()
 }
 </script>
+
+<style scoped>
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.dialog-title__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.dialog-title__content {
+  min-width: 0;
+}
+
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.inspect-item-form {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+  padding-top: 8px;
+}
+
+.form-section {
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 42px;
+  padding: 0 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-heading span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  column-gap: 24px;
+  padding: 18px 18px 2px;
+}
+
+.form-grid__full {
+  grid-column: 1 / -1;
+}
+
+.form-content {
+  padding: 18px 18px 2px;
+}
+
+.inspect-item-form :deep(.el-form-item) {
+  margin-bottom: 16px;
+}
+
+.inspect-item-form :deep(.el-select),
+.inspect-item-form :deep(.el-tree-select),
+.inspect-item-form :deep(.el-input) {
+  width: 100%;
+}
+
+@media (width <= 768px) {
+  .dialog-title__subtitle {
+    display: none;
+  }
+
+  .form-grid {
+    grid-template-columns: minmax(0, 1fr);
+    padding: 14px 14px 0;
+  }
+
+  .form-grid__full {
+    grid-column: auto;
+  }
+
+  .form-content {
+    padding: 14px 14px 0;
+  }
+}
+</style>

+ 281 - 148
src/views/pms/inspect/item/index.vue

@@ -1,150 +1,152 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm px-2"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('deviceForm.category')" prop="deviceClassify" style="width: 15vw">
-            <el-tree-select
-              v-model="queryParams.deviceClassify"
-              :data="productClassifyList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              :placeholder="t('deviceForm.categoryHolder')"
-              filterable
-            />
-          </el-form-item>
-          <el-form-item :label="t('inspect.InspectionItems')" prop="item" style="margin-left: 30px">
-            <el-input
-              v-model="queryParams.item"
-              :placeholder="t('inspect.itemHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
-            <el-date-picker
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="daterange"
-              :start-placeholder="t('operationFill.start')"
-              :end-placeholder="t('operationFill.end')"
-              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-              class="!w-220px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-inspect-item:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-inspect-item:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="inspect-item-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="inspect-item-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="88px"
+      class="inspect-item-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('deviceForm.category')" prop="deviceClassify">
+          <el-tree-select
+            v-model="queryParams.deviceClassify"
+            :data="productClassifyList"
+            :props="defaultProps"
+            check-strictly
+            node-key="id"
+            :placeholder="t('deviceForm.categoryHolder')"
+            filterable
+            clearable
+            class="query-control" />
+        </el-form-item>
+        <el-form-item :label="t('inspect.InspectionItems')" prop="item">
+          <el-input
+            v-model="queryParams.item"
+            :placeholder="t('inspect.itemHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            :start-placeholder="t('operationFill.start')"
+            :end-placeholder="t('operationFill.end')"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="query-control query-control--date" />
+        </el-form-item>
+      </div>
+
+      <el-form-item class="query-actions">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('operationFill.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('operationFill.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['rq:iot-inspect-item:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-inspect-item:export']">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div
+      class="table-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+              settings-cache-key="pms-inspect-item-list">
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn :label="t('monitor.category')" prop="deviceClassifyName" />
+              <ZmTableColumn :label="t('iotDevice.brand')" prop="brandName" />
+              <ZmTableColumn :label="t('iotDevice.code')" prop="deviceCode" />
+              <ZmTableColumn :label="t('monitor.deviceName')" prop="deviceName" />
+              <ZmTableColumn :label="t('inspect.InspectionItems')" prop="item" />
+              <ZmTableColumn :label="t('inspect.InspectionStandards')" prop="standard" />
+              <ZmTableColumn :label="t('inspect.createName')" prop="createName" />
+              <ZmTableColumn
+                :label="t('common.createTime')"
+                prop="createTime"
+                :formatter="dateFormatter" />
+              <ZmTableColumn
+                :label="t('operationFill.operation')"
+                :width="120"
+                action
+                fixed="right">
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-item:update']">
+                    {{ t('fault.edit') }}
+                  </el-button>
+                  <el-button
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-item:delete']">
+                    {{ t('fault.del') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list" show-border height="calc(85vh - 180px)">
-          <!--      <el-table-column label="主键id" align="center" prop="id" />-->
-          <zm-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('monitor.category')"
-            align="center"
-            prop="deviceClassifyName"
-          />
-          <zm-table-column :label="t('iotDevice.brand')" align="center" prop="brandName" />
-          <zm-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />
-          <zm-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName" />
-          <zm-table-column :label="t('inspect.InspectionItems')" align="center" prop="item" />
-          <zm-table-column
-            :label="t('inspect.InspectionStandards')"
-            align="center"
-            prop="standard"
-          />
-          <zm-table-column :label="t('inspect.createName')" align="center" prop="createName" />
-          <!--      <el-table-column :label="t('common.sort')" align="center" prop="sort" />-->
-          <!--      <el-table-column :label="t('form.remark')" align="center" prop="remark" />-->
-
-          <zm-table-column
-            :label="t('common.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180px"
-          />
-          <!--      <el-table-column label="部门id" align="center" prop="deptId" />-->
-          <zm-table-column
-            :label="t('operationFill.operation')"
-            align="center"
-            min-width="120px"
-            action
-            fixed="right"
-          >
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-item:update']"
-              >
-                {{ t('fault.edit') }}
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-item:delete']"
-              >
-                {{ t('fault.del') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
-        <Pagination
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
 
   <!-- 表单弹窗:添加/修改 -->
   <IotInspectItemForm ref="formRef" @success="getList" />
@@ -157,17 +159,18 @@ import { IotInspectItemApi, IotInspectItemVO } from '@/api/pms/inspect/item'
 import IotInspectItemForm from './IotInspectItemForm.vue'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+import { useUserStore } from '@/store/modules/user'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotInspectItemVO>()
 
 /** 巡检项 列表 */
 defineOptions({ name: 'IotInspectItem' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-let isLeftContentCollapsed = ref(false)
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 const loading = ref(true) // 列表的加载中
 const list = ref<IotInspectItemVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
@@ -185,8 +188,9 @@ const queryParams = reactive({
 const productClassifyList = ref<Tree[]>([]) // 树形结构
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
-const handleDeptNodeClick = async (row) => {
+const handleDeptNodeClick = async (row: Tree) => {
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 /** 查询列表 */
@@ -209,10 +213,20 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+const handleSizeChange = (pageSize: number) => {
+  queryParams.pageSize = pageSize
   handleQuery()
 }
 
+const handleCurrentChange = (pageNo: number) => {
+  queryParams.pageNo = pageNo
+  getList()
+}
+
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
@@ -255,3 +269,122 @@ onMounted(async () => {
   await getList()
 })
 </script>
+
+<style scoped>
+.inspect-item-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  min-width: 0;
+  gap: 12px 24px;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+.query-control--date {
+  width: 230px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+@media (width >= 1800px) {
+  .inspect-item-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .inspect-item-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 175px;
+  }
+
+  .query-control--date {
+    width: 220px;
+  }
+}
+
+@media (width <= 1200px) {
+  .inspect-item-page {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  :deep(.inspect-item-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .inspect-item-query,
+  .table-panel {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions,
+  .query-control,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+  }
+}
+</style>

+ 348 - 176
src/views/pms/inspect/order/InspectOrderDetail.vue

@@ -1,204 +1,376 @@
 <template>
-  <ContentWrap v-loading="formLoading">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      v-loading="formLoading"
-      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-      label-width="130px"
-    >
-      <div class="base-expandable-content">
-        <el-row>
-          <el-col :span="12">
-            <el-form-item :label="t('main.planCode')" prop="planCode">
-              <el-input v-model="formData.planCode" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item :label="t('bomList.name')" prop="inspectOrderTitle">
-              <el-input v-model="formData.inspectOrderTitle" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item :label="t('bomList.code')" prop="inspectOrderCode">
-              <el-input v-model="formData.inspectOrderCode" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item :label="t('iotMaintain.PersonInCharge')" prop="chargeName">
-              <el-select v-model="formData.chargeName" style="width: 100%" disabled />
-              <el-option
-                v-for="item in deptUsers"
-                :key="item.id"
-                :label="item.nickname"
-                :value="item.id"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item :label="t('inspect.reason')" prop="reason">
-              <el-input type="textarea" v-model="formData.reason" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item :label="t('iotMaintain.remark')" prop="remark">
-              <el-input type="textarea" v-model="formData.remark" disabled />
-            </el-form-item>
-          </el-col>
-        </el-row>
+  <div class="inspect-order-detail-page" v-loading="formLoading">
+    <header class="page-header">
+      <div class="page-header__main">
+        <div class="page-header__icon"><Icon icon="ep:document" /></div>
+        <div class="page-header__content">
+          <h1>{{ formData.inspectOrderTitle || '巡检工单详情' }}</h1>
+          <p>查看工单基础信息及设备巡检结果统计</p>
+        </div>
       </div>
-    </el-form>
-  </ContentWrap>
-  <ContentWrap>
-    <!-- 列表 -->
-      <ContentWrap>
-        <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-          <el-table-column :label="t('iotMaintain.deviceCode')" align="center" prop="deviceCode" />
-          <el-table-column :label="t('iotMaintain.deviceName')" align="center" prop="deviceName" />
-          <el-table-column :label="t('inspect.itemNeed')" align="center" prop="itemNeed" width="180" >
-            <template #default="scope">
-              <el-tag  type="info"> {{scope.row.itemNeed}}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('inspect.itemNormal')" align="center" prop="itemNormal" width="180" >
-            <template #default="scope">
-              <el-tag  type="success"> {{scope.row.itemNormal}}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('inspect.itemException')" align="center" prop="itemException" width="180" >
-            <template #default="scope">
-              <el-tag  type="danger"> {{scope.row.itemException}}</el-tag>
-            </template>
-          </el-table-column>
-<!--          <el-table-column label="路线名称" align="center" prop="routeName" />-->
-          <el-table-column :label="t('iotMaintain.operation')" align="center" width="200px">
-            <template #default="scope">
-              <div style="display: flex; justify-content: center; align-items: center; width: 100%">
-                <div style="margin-right: 10px">
-                  <Icon style="vertical-align: middle; color: #c36e5b" icon="ep:view" />
-                  <el-button
-                    style="vertical-align: middle"
-                    link
-                    type="warning"
-                    @click="viewRoute(scope.row.orderDetails)"
-                  >
-                    {{ t('route.InspectionRoute') }}
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </header>
+
+    <section class="detail-section">
+      <header class="section-heading"><span></span>工单信息</header>
+      <div class="detail-grid">
+        <div class="detail-item">
+          <span class="detail-item__label">{{ t('main.planCode') }}</span>
+          <span class="detail-item__value">{{ formData.planCode || '-' }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="detail-item__label">{{ t('bomList.name') }}</span>
+          <span class="detail-item__value">{{ formData.inspectOrderTitle || '-' }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="detail-item__label">{{ t('bomList.code') }}</span>
+          <span class="detail-item__value">{{ formData.inspectOrderCode || '-' }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="detail-item__label">{{ t('iotMaintain.PersonInCharge') }}</span>
+          <span class="detail-item__value">{{ formData.chargeName || '-' }}</span>
+        </div>
+        <div class="detail-item detail-item--multiline">
+          <span class="detail-item__label">{{ t('inspect.reason') }}</span>
+          <span class="detail-item__value">{{ formData.reason || '-' }}</span>
+        </div>
+        <div class="detail-item detail-item--multiline">
+          <span class="detail-item__label">{{ t('iotMaintain.remark') }}</span>
+          <span class="detail-item__value">{{ formData.remark || '-' }}</span>
+        </div>
+      </div>
+    </section>
+
+    <section class="detail-section device-section">
+      <header class="section-heading section-heading--between">
+        <div class="section-heading__title"><span></span>巡检设备</div>
+        <div class="device-count">共 {{ list.length }} 台</div>
+      </header>
+      <div class="table-body">
+        <el-auto-resizer class="table-resizer">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+              settings-cache-key="pms-inspect-order-detail-device-list">
+              <ZmTableColumn :label="t('iotMaintain.deviceCode')" prop="deviceCode" />
+              <ZmTableColumn :label="t('iotMaintain.deviceName')" prop="deviceName" />
+              <ZmTableColumn :label="t('inspect.itemNeed')" prop="itemNeed">
+                <template #default="scope">
+                  <el-tag type="info">{{ scope.row.itemNeed }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('inspect.itemNormal')" prop="itemNormal">
+                <template #default="scope">
+                  <el-tag type="success">{{ scope.row.itemNormal }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('inspect.itemException')" prop="itemException">
+                <template #default="scope">
+                  <el-tag type="danger">{{ scope.row.itemException }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('iotMaintain.operation')"
+                :width="160"
+                fixed="right"
+                hide-in-column-settings
+                action>
+                <template #default="scope">
+                  <el-button link type="primary" @click="viewRoute(scope.row.orderDetails)">
+                    <Icon icon="ep:view" class="mr-5px" />{{ t('route.InspectionRoute') }}
                   </el-button>
-                </div>
-              </div>
-            </template>
-          </el-table-column>
-        </el-table>
-      </ContentWrap>
-  </ContentWrap>
-  <ContentWrap>
-    <el-form>
-      <el-form-item style="float: right">
-        <el-button @click="close">{{t('common.cancel')}}</el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-  <RouteInspectItemDrawer
-    ref="showDrawer"
-    :model-value="drawerVisible"
-    @update:model-value="(val) => (drawerVisible = val)"
-  />
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </section>
+
+    <footer class="action-bar">
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </footer>
+
+    <RouteInspectItemDrawer
+      ref="showDrawer"
+      :model-value="drawerVisible"
+      @update:model-value="(val) => (drawerVisible = val)" />
+  </div>
 </template>
+
 <script setup lang="ts">
-import * as UserApi from '@/api/system/user'
-import { ref } from 'vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import { useCache } from '@/hooks/web/useCache'
 import RouteInspectItemDrawer from '@/views/pms/inspect/plan/RouteInspectItemDrawer.vue'
 import { IotInspectOrderApi, IotInspectOrderVO } from '@/api/pms/inspect/order'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+type InspectOrderDeviceRow = {
+  deviceCode: string
+  deviceName: string
+  itemNeed: number
+  itemNormal: number
+  itemException: number
+  orderDetails: unknown[]
+}
+
+type InspectOrderDetail = IotInspectOrderVO & {
+  planCode?: string
+  chargeName?: string
+  reason?: string
+}
+
+const { ZmTable, ZmTableColumn } = useTableComponents<InspectOrderDeviceRow>()
 
-/** 维修工单 表单 */
-defineOptions({ name: 'IotMaintainAe' })
+defineOptions({ name: 'InspectOrderDetail' })
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const { delView } = useTagsViewStore() // 视图操作
+const { t } = useI18n()
+const { delView } = useTagsViewStore()
 const { currentRoute, push } = useRouter()
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const drawerVisible = ref<boolean>(false)
-const showDrawer = ref()
-const list = ref<IotInspectOrderVO[]>([]) // 列表的数据
-const { params, name } = useRoute() // 查询参数
+const { params } = useRoute()
 const id = params.id
-const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
-const formData = ref({
-  planCode: undefined,
-  id: undefined,
-  inspectOrderTitle: undefined,
-  inspectOrderCode: undefined,
-  status: undefined,
-  remark: undefined,
-  deptId: undefined,
-  reason:undefined,
-  deviceIds: undefined
-})
-const formRules = reactive({
-  planTitle: [{ required: true, message: '巡检计划标题不能为空', trigger: 'blur' }],
-  planCode: [{ required: true, message: '巡检计划编号不能为空', trigger: 'blur' }],
-  planUnit: [{ required: true, message: '单位不能为空', trigger: 'blur' }],
-  charges: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
-})
-// 拖动状态管理
-const items = ref([])
-const deviceChoose = (rows) => {
-  list.value = rows
-}
 
-const viewRoute = (itemJson) => {
+const formLoading = ref(false)
+const loading = ref(false)
+const drawerVisible = ref(false)
+const showDrawer = ref()
+const list = ref<InspectOrderDeviceRow[]>([])
+const formData = ref<Partial<InspectOrderDetail>>({})
+
+const viewRoute = (orderDetails: unknown[]) => {
   drawerVisible.value = true
-  showDrawer.value.openDrawer(itemJson)
-}
-const formRef = ref() // 表单 Ref
-const inspectItemChoose = (rows) => {
-  items.value = []
-  items.value = rows
-}
-const deviceFormRef = ref()
-const openForm = () => {
-  deviceFormRef.value.open()
+  showDrawer.value.openDrawer(orderDetails)
 }
 
 const close = () => {
   delView(unref(currentRoute))
   push({ name: 'IotInspectOrder', params: {} })
 }
-const { wsCache } = useCache()
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 
-/** 重置表单 */
 onMounted(async () => {
-  if (id) {
-    formType.value = 'update'
-    const iotInspectOrder = await IotInspectOrderApi.getIotInspectOrder(id)
-    formData.value = iotInspectOrder
-    list.value = iotInspectOrder.details
-  } else {
-    formType.value = 'create'
+  if (!id) return
+
+  formLoading.value = true
+  loading.value = true
+  try {
+    const inspectOrder = (await IotInspectOrderApi.getIotInspectOrder(id)) as InspectOrderDetail
+    formData.value = inspectOrder
+    list.value = (inspectOrder.details || []) as InspectOrderDeviceRow[]
+  } finally {
+    formLoading.value = false
+    loading.value = false
   }
 })
-const handleDelete = async (id: number) => {
-  try {
-    const index = list.value.findIndex((item) => item.code === id)
-    if (index !== -1) {
-      // 通过 splice 删除元素
-      list.value.splice(index, 1)
-    }
-  } catch {}
-}
 </script>
+
 <style scoped>
-.base-expandable-content {
-  overflow: hidden; /* 隐藏溢出的内容 */
-  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+@media (width <= 1400px) {
+  .inspect-order-detail-page .detail-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (width <= 768px) {
+  .inspect-order-detail-page .page-header {
+    align-items: flex-start;
+  }
+
+  .inspect-order-detail-page .page-header p {
+    display: none;
+  }
+
+  .inspect-order-detail-page .detail-grid {
+    grid-template-columns: minmax(0, 1fr);
+    padding: 12px;
+  }
+
+  .inspect-order-detail-page .detail-item {
+    grid-template-columns: 96px minmax(0, 1fr);
+  }
+}
+
+.inspect-order-detail-page {
+  display: flex;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  flex-direction: column;
+  gap: 12px;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 66px;
+  padding: 12px 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.page-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.page-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.page-header__content {
+  min-width: 0;
+}
+
+.page-header h1 {
+  margin: 0;
+  overflow: hidden;
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.page-header p {
+  margin: 1px 0 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.detail-section {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-lighter);
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-heading > span,
+.section-heading__title > span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.section-heading--between {
+  justify-content: space-between;
+}
+
+.section-heading__title {
+  display: flex;
+  align-items: center;
+}
+
+.device-count {
+  font-size: 13px;
+  font-weight: 400;
+  color: var(--el-text-color-secondary);
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  padding: 16px;
+  gap: 12px 24px;
+}
+
+.detail-item {
+  display: grid;
+  grid-template-columns: 120px minmax(0, 1fr);
+  min-height: 38px;
+  padding: 8px 10px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  align-items: start;
+}
+
+.detail-item__label {
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
+
+.detail-item__value {
+  min-width: 0;
+  font-weight: 500;
+  color: var(--el-text-color-primary);
+  overflow-wrap: anywhere;
+}
+
+.detail-item--multiline .detail-item__value {
+  white-space: pre-wrap;
+}
+
+.device-section {
+  display: flex;
+  min-height: 320px;
+  flex: 1;
+  flex-direction: column;
+}
+
+.table-body {
+  position: relative;
+  min-width: 0;
+  min-height: 260px;
+  overflow: hidden;
+  box-sizing: border-box;
+  flex: 1;
+}
+
+.table-resizer {
+  position: absolute;
+  inset: 12px 12px 18px;
+  width: auto !important;
+  height: auto !important;
+  min-width: 0;
+}
+
+.action-bar {
+  position: sticky;
+  bottom: 0;
+  z-index: 5;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  min-height: 56px;
+  padding: 10px 16px;
+  margin-top: auto;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 -4px 12px rgb(0 0 0 / 5%);
 }
-/* 横向布局容器 */
 </style>

+ 938 - 153
src/views/pms/inspect/order/WriteOrder.vue

@@ -1,134 +1,183 @@
 <template>
-  <ContentWrap>
-    <el-tabs
-      v-model="activeTab"
-      type="border-card"
-      tab-position="left"
-      v-loading="loading"
-      style="height: 84vh"
-    >
-      <el-tab-pane
-        style="height: 100%"
-        v-for="(tab, tabIndex) in tabs"
-        :key="tab.deviceName"
-        :name="String(tabIndex)"
-      >
-        <template #label>
-          <span class="custom-label">
-            {{ tab.deviceName }} ({{ completedSteps(tabIndex) }}/{{ tab.orderDetails.length }})
-                  <el-tooltip :content="tab.routeDesc" placement="top" effect="dark">
-                    <Icon icon="ep:info-filled" />
-                  </el-tooltip>
-          </span>
-        </template>
-
-        <div class="step-container">
-          <el-steps direction="vertical" :active="currentStep[tabIndex]" class="steps-nav">
-            <el-step
+  <div class="write-order-page" v-loading="loading">
+    <header class="page-header">
+      <div class="page-header__main">
+        <div class="page-header__icon"><Icon icon="ep:edit-pen" /></div>
+        <div>
+          <h1>填写巡检工单</h1>
+          <p>按设备和巡检项顺序填写检查结果与异常信息</p>
+        </div>
+      </div>
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </header>
+
+    <el-empty v-if="!loading && tabs.length === 0" class="empty-panel" description="暂无巡检设备" />
+    <div v-else class="inspection-workbench">
+      <aside class="device-panel">
+        <header class="device-panel__header">
+          <div>
+            <strong>巡检设备</strong>
+            <span>共 {{ tabs.length }} 台</span>
+          </div>
+        </header>
+        <div class="device-list">
+          <button
+            v-for="(tab, tabIndex) in tabs"
+            :key="`${tab.deviceId}-${tabIndex}`"
+            type="button"
+            class="device-item"
+            :class="{ 'is-active': activeTab === String(tabIndex) }"
+            @click="activeTab = String(tabIndex)">
+            <span class="device-item__icon"><Icon icon="ep:monitor" /></span>
+            <span class="device-item__content">
+              <span class="device-item__name">{{ tab.deviceName }}</span>
+              <span class="device-item__progress">
+                已完成 {{ completedSteps(tabIndex) }} / {{ tab.orderDetails.length }} 项
+              </span>
+            </span>
+            <Icon icon="ep:arrow-right" class="device-item__arrow" />
+          </button>
+        </div>
+      </aside>
+
+      <div class="device-content">
+        <section
+          v-for="(tab, tabIndex) in tabs"
+          v-show="activeTab === String(tabIndex)"
+          :key="`${tab.deviceId}-content`"
+          class="device-inspection-panel">
+          <header class="device-summary">
+            <div class="device-summary__main">
+              <div class="device-summary__icon"><Icon icon="ep:monitor" /></div>
+              <div>
+                <h2>{{ tab.deviceName }}</h2>
+                <p>{{ tab.routeDesc || '按巡检项顺序完成当前设备检查' }}</p>
+              </div>
+            </div>
+            <span class="device-summary__progress">
+              {{ completedSteps(tabIndex) }}/{{ tab.orderDetails.length }} 已完成
+            </span>
+          </header>
+
+          <div class="item-progress-strip">
+            <div
               v-for="(step, stepIndex) in tab.orderDetails"
               :key="stepIndex"
-              :title="`${step.item}`"
-              :status="getStepStatus(tabIndex, stepIndex)"
-            >
-              <template #title>
-                <div class="step-title-wrapper">
-                  <span class="title-text">{{ step.item }}</span>
-                  <el-tooltip :content="step.standard" placement="top" effect="dark">
-                    <Icon icon="ep:info-filled" />
-                  </el-tooltip>
+              class="progress-item"
+              :class="`is-${getStepStatus(tabIndex, stepIndex)}`">
+              <span class="progress-item__index">
+                <Icon v-if="getStepStatus(tabIndex, stepIndex) === 'finish'" icon="ep:check" />
+                <template v-else>{{ stepIndex + 1 }}</template>
+              </span>
+              <span class="progress-item__name">{{ step.item }}</span>
+            </div>
+          </div>
+
+          <div class="current-item-layout">
+            <section class="entry-section">
+              <header class="section-heading section-heading--between">
+                <div class="section-heading__title">
+                  <span></span>
+                  {{ tab.orderDetails[currentStep[tabIndex]].item }}
+                </div>
+                <div class="step-counter">
+                  第 {{ currentStep[tabIndex] + 1 }} / {{ tab.orderDetails.length }} 项
                 </div>
-              </template>
-            </el-step>
-          </el-steps>
+              </header>
 
-          <div class="form-wrapper">
-            <ContentWrap>
               <el-form
                 :model="formData[tabIndex][currentStep[tabIndex]]"
                 :rules="formRules"
                 ref="formRefs"
-                label-width="120px"
-              >
+                size="default"
+                label-width="96px"
+                class="inspection-form">
                 <el-form-item label="设备id" v-if="false" prop="deviceId">
                   <el-input
                     v-model="formData[tabIndex][currentStep[tabIndex]].deviceId"
                     :model-value="tab.deviceId"
-                    clearable
-                  />
-                </el-form-item>
-                <el-form-item :label="t('inspect.isException')" prop="ifNormal">
-                  <el-select
-                    v-model="formData[tabIndex][currentStep[tabIndex]].ifNormal"
-                    :placeholder="t('inspect.isException')"
-                    clearable
-                  >
-                    <el-option
-                      v-for="dict in getBoolDictOptions(DICT_TYPE.PMS_INSPECT_WRITE)"
-                      :key="dict.value"
-                      :label="dict.label"
-                      :value="dict.value"
-                    />
-                  </el-select>
-                </el-form-item>
-
-                <el-form-item
-                  :label="t('inspect.exceptionDes')"
-                  prop="description"
-                  :rules="formData[tabIndex][currentStep[tabIndex]].ifNormal ?  []: descriptionRule"
-                >
-                  <el-input
-                    v-model="formData[tabIndex][currentStep[tabIndex]].description"
-                    type="textarea"
-                    :rows="5"
-                    :placeholder="t('inspect.exceptionDes')"
-                  />
-                </el-form-item>
-
-                <el-form-item :label="t('deviceForm.picture')" prop="picUrl">
-                  <UploadImg
-                    v-model="formData[tabIndex][currentStep[tabIndex]].picUrl"
-                    height="55px"
-                    width="30em"
-                  />
+                    clearable />
                 </el-form-item>
+                <div class="form-grid">
+                  <el-form-item :label="t('inspect.isException')" prop="ifNormal">
+                    <el-select
+                      v-model="formData[tabIndex][currentStep[tabIndex]].ifNormal"
+                      :placeholder="t('inspect.isException')"
+                      clearable>
+                      <el-option
+                        v-for="dict in getBoolDictOptions(DICT_TYPE.PMS_INSPECT_WRITE)"
+                        :key="dict.value"
+                        :label="dict.label"
+                        :value="dict.value" />
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item :label="t('deviceForm.picture')" prop="picUrl">
+                    <UploadImg
+                      v-model="formData[tabIndex][currentStep[tabIndex]].picUrl"
+                      height="55px"
+                      width="120px" />
+                  </el-form-item>
+                  <el-form-item
+                    :label="t('inspect.exceptionDes')"
+                    prop="description"
+                    class="form-grid__wide"
+                    :rules="
+                      formData[tabIndex][currentStep[tabIndex]].ifNormal ? [] : descriptionRule
+                    ">
+                    <el-input
+                      v-model="formData[tabIndex][currentStep[tabIndex]].description"
+                      type="textarea"
+                      :rows="5"
+                      :placeholder="t('inspect.exceptionDes')" />
+                  </el-form-item>
+                </div>
               </el-form>
+            </section>
 
-              <div class="navigation-controls">
-                <el-button :disabled="currentStep[tabIndex] === 0" @click="handlePrev(tabIndex)">
-                  上一步
-                </el-button>
-
-                <el-button
-                  type="primary"
-                  :disabled="!isStepValid(tabIndex)"
-                  @click="handleNext(tabIndex, tab.deviceId)"
-                >
-                  {{ isLastStep(tabIndex) ? '完成提交' : '下一步' }}
-                </el-button>
+            <aside class="standard-section">
+              <header class="standard-section__header">
+                <Icon icon="ep:notebook-2" />
+                <span>{{ t('inspect.InspectionStandards') }}</span>
+              </header>
+              <div class="standard-section__content">
+                {{ tab.orderDetails[currentStep[tabIndex]].standard || '-' }}
               </div>
-            </ContentWrap>
-            <ContentWrap style="margin-top: 50px">
-              <el-card>
-                <span style="font-weight: bold">{{ t('inspect.InspectionStandards') }}:</span
-                >{{ tab.orderDetails[currentStep[tabIndex]].standard }}<br />
-                <el-divider />
-                <span style="font-weight: bold">{{ t('fileInfo.appendix') }}:</span>
+              <footer class="standard-section__footer">
+                <span>{{ t('fileInfo.appendix') }}</span>
                 <el-button
                   v-if="tab.orderDetails[currentStep[tabIndex]].urls"
                   link
                   type="primary"
-                  @click="openWeb(tab.orderDetails[currentStep[tabIndex]].urls)"
-                >
-                  <Icon size="19" icon="ep:view" />
-                  {{ t('action.preview') }}
+                  @click="openWeb(tab.orderDetails[currentStep[tabIndex]].urls)">
+                  <Icon icon="ep:view" class="mr-5px" />{{ t('action.preview') }}
                 </el-button>
-              </el-card>
-            </ContentWrap>
+                <span v-else>-</span>
+              </footer>
+            </aside>
           </div>
-        </div>
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
+
+          <div class="navigation-controls">
+            <el-button
+              size="default"
+              :disabled="currentStep[tabIndex] === 0"
+              @click="handlePrev(tabIndex)">
+              <Icon icon="ep:arrow-left" class="mr-5px" />上一步
+            </el-button>
+            <el-button
+              size="default"
+              type="primary"
+              :disabled="!isStepValid(tabIndex)"
+              @click="handleNext(tabIndex, tab.deviceId)">
+              {{ isLastStep(tabIndex) ? '完成提交' : '下一步' }}
+              <Icon :icon="isLastStep(tabIndex) ? 'ep:check' : 'ep:arrow-right'" class="ml-5px" />
+            </el-button>
+          </div>
+        </section>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup>
@@ -160,6 +209,11 @@ const openWeb = (url) => {
     'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
   )
 }
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotInspectOrder', params: {} })
+}
 // 响应式状态
 const loading = ref(true)
 const activeTab = ref('0')
@@ -265,78 +319,809 @@ const submitForm = (tabIndex, current, type) => {
 </script>
 
 <style scoped>
-.step-container {
+@media (width <= 1400px) {
+  .write-order-page .device-tabs :deep(.el-tabs__header.is-left) {
+    width: 210px;
+  }
+
+  .write-order-page .inspection-workspace {
+    grid-template-columns: 220px minmax(0, 1fr);
+  }
+}
+
+@media (width <= 1100px) {
+  .write-order-page .inspection-workspace {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: 220px minmax(0, 1fr);
+  }
+
+  .write-order-page .steps-panel {
+    border-right: 0;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+}
+
+@media (width <= 768px) {
+  .write-order-page .page-header {
+    align-items: flex-start;
+  }
+
+  .write-order-page .page-header p {
+    display: none;
+  }
+
+  .write-order-page .device-tabs :deep(.el-tabs__header.is-left) {
+    width: 170px;
+  }
+
+  .write-order-page .custom-label__progress {
+    display: none;
+  }
+
+  .write-order-page .step-panel__content {
+    padding: 16px 12px 0;
+  }
+
+  .write-order-page .inspection-form {
+    max-width: none;
+  }
+
+  .write-order-page .standard-panel {
+    padding: 12px;
+  }
+}
+
+.write-order-page {
+  display: flex;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  flex-direction: column;
+  gap: 12px;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 66px;
+  padding: 12px 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.page-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.page-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.page-header h1 {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.page-header p {
+  margin: 1px 0 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.empty-panel {
+  min-height: 480px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+  flex: 1;
+}
+
+.device-tabs {
+  display: flex;
+  min-height: 580px;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+  flex: 1;
+}
+
+.device-tabs :deep(.el-tabs__header.is-left) {
+  width: 240px;
+  margin-right: 0;
+  overflow: hidden;
+  background: var(--el-fill-color-extra-light);
+  flex: 0 0 auto;
+}
+
+.device-tabs :deep(.el-tabs__nav-wrap.is-left) {
+  height: 100%;
+}
+
+.device-tabs :deep(.el-tabs__item.is-left) {
+  justify-content: flex-start;
+  height: 48px;
+  padding: 0 14px;
+}
+
+.device-tabs :deep(.el-tabs__content) {
+  height: 100%;
+  min-width: 0;
+  padding: 0;
+  overflow: hidden;
+  flex: 1;
+}
+
+.device-tabs :deep(.el-tab-pane) {
+  height: 100%;
+}
+
+.custom-label {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  min-width: 0;
+  gap: 8px;
+}
+
+.custom-label__name {
+  min-width: 0;
+  overflow: hidden;
+  font-weight: 500;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+}
+
+.custom-label__progress {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+  flex: 0 0 auto;
+}
+
+.custom-label__info,
+.step-info {
+  color: var(--el-color-primary);
+  flex: 0 0 auto;
+}
+
+.inspection-workspace {
   display: grid;
-  grid-template-columns: 220px 1fr;
-  gap: 10px;
+  grid-template-columns: 260px minmax(0, 1fr);
   height: 100%;
-  min-height: 600px;
+  min-height: 0;
 }
 
-.steps-nav {
+.steps-panel {
+  display: flex;
+  min-width: 0;
+  overflow: hidden;
+  background: var(--el-fill-color-blank);
+  border-right: 1px solid var(--el-border-color-lighter);
+  flex-direction: column;
+}
+
+.steps-panel__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 44px;
+  padding: 0 16px;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.steps-panel__header span:last-child {
+  font-size: 12px;
+  font-weight: 400;
+  color: var(--el-text-color-secondary);
+}
+
+.steps-panel__body {
+  min-height: 0;
+  padding: 18px 12px 10px 18px;
   overflow-y: auto;
-  padding-right: 15px;
+  flex: 1;
 }
 
-.form-wrapper {
-  background: #fff;
-  padding: 30px;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+.steps-nav {
+  min-height: 100%;
 }
 
-.navigation-controls {
-  margin-top: 40px;
-  text-align: center;
+.steps-nav :deep(.el-step__title) {
+  min-width: 0;
+  font-size: 14px;
+  line-height: 22px;
 }
 
-.custom-label {
-  font-weight: 1000;
-  font-size: 17px;
-  padding: 0 10px;
+.steps-nav :deep(.el-step__head.is-process .el-step__icon) {
+  color: #fff;
+  background: var(--el-color-primary);
+  border-color: var(--el-color-primary);
 }
-::v-deep .el-step__icon {
-  background-color: #409eff;
+
+.steps-nav :deep(.el-step__head.is-finish .el-step__icon) {
   color: #fff;
-  border: 0px;
+  background: var(--el-color-success);
+  border-color: var(--el-color-success);
 }
+
 .step-title-wrapper {
-  display: inline-flex;
+  display: flex;
+  align-items: flex-start;
+  min-width: 0;
+  padding-right: 8px;
+  gap: 6px;
+}
+
+.title-text {
+  min-width: 0;
+  overflow-wrap: anywhere;
+}
+
+.step-panel {
+  display: flex;
+  min-width: 0;
+  min-height: 0;
+  flex-direction: column;
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-heading--between {
+  justify-content: space-between;
+}
+
+.section-heading__title {
+  display: flex;
   align-items: center;
+  min-width: 0;
+}
+
+.section-heading__title > span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+  flex: 0 0 auto;
+}
+
+.step-counter {
+  margin-left: 16px;
+  font-size: 13px;
+  font-weight: 400;
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
+
+.step-panel__content {
+  display: flex;
+  min-height: 0;
+  padding: 24px 24px 0;
+  overflow-y: auto;
+  flex: 1;
+  flex-direction: column;
+}
+
+.inspection-form {
+  width: 100%;
+  max-width: 920px;
+}
+
+.inspection-form :deep(.el-form-item) {
+  margin-bottom: 20px;
+}
+
+.inspection-form :deep(.el-select),
+.inspection-form :deep(.el-input) {
+  width: 100%;
+}
+
+.standard-panel {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr);
+  padding: 14px 16px;
+  margin-top: 4px;
+  background: var(--el-fill-color-extra-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+  gap: 10px;
+}
+
+.standard-item {
+  display: flex;
+  min-width: 0;
+  align-items: flex-start;
+}
+
+.standard-item__label {
+  width: 110px;
+  margin-right: 10px;
+  font-weight: 600;
+  color: var(--el-text-color-regular);
+  flex: 0 0 auto;
+}
+
+.standard-item__value {
+  min-width: 0;
+  color: var(--el-text-color-secondary);
+  overflow-wrap: anywhere;
+}
+
+.navigation-controls {
+  position: sticky;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  min-height: 60px;
+  padding: 10px 0;
+  margin-top: auto;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
   gap: 8px;
-  position: relative;
-  padding-right: 25px;
 }
 
-/* 覆盖步骤条默认样式 */
-:deep(.custom-steps) {
-  /* 调整头部位置 */
-  .el-step__head {
-    top: 3px;
+.navigation-controls :deep(.el-button) {
+  margin-left: 0;
+}
+
+@media (width <= 1400px) {
+  .write-order-page .inspection-workbench {
+    grid-template-columns: 250px minmax(0, 1fr);
+  }
+
+  .write-order-page .current-item-layout {
+    grid-template-columns: minmax(0, 1fr) 380px;
   }
+}
+
+@media (width <= 1200px) {
+  .write-order-page .current-item-layout {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .write-order-page .standard-section__content {
+    max-height: 220px;
+  }
+}
 
-  /* 标题容器定位 */
-  .el-step__title {
-    display: inline-block;
-    margin-left: 10px;
-    padding-right: 0;
+@media (width <= 900px) {
+  .write-order-page .inspection-workbench {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto minmax(520px, 1fr);
   }
 
-  /* 步骤连接线 */
-  .el-step__line {
-    left: 11px;
-    background-color: #ebeef5;
+  .write-order-page .device-panel {
+    border-right: 0;
+    border-bottom: 1px solid var(--el-border-color-lighter);
   }
 
-  /* 当前步骤样式 */
-  .is-process .title-text {
-    font-weight: 600;
-    color: #409eff;
+  .write-order-page .device-list {
+    flex-direction: row;
+    padding: 10px;
+    overflow-x: auto;
   }
 
-  /* 完成状态图标 */
-  .is-finish .tip-icon {
-    color: #67c23a;
+  .write-order-page .device-item {
+    width: 240px;
+    flex: 0 0 auto;
   }
 }
+
+@media (width <= 600px) {
+  .write-order-page .device-summary,
+  .write-order-page .section-heading--between {
+    align-items: flex-start;
+  }
+
+  .write-order-page .device-summary__progress,
+  .write-order-page .step-counter {
+    margin-top: 3px;
+  }
+
+  .write-order-page .current-item-layout {
+    padding: 12px;
+  }
+
+  .write-order-page .form-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .write-order-page .form-grid__wide {
+    grid-column: auto;
+  }
+}
+
+.inspection-workbench {
+  display: grid;
+  grid-template-columns: 280px minmax(0, 1fr);
+  min-height: 580px;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+  flex: 1;
+}
+
+.device-panel {
+  display: flex;
+  min-width: 0;
+  overflow: hidden;
+  background: var(--el-fill-color-extra-light);
+  border-right: 1px solid var(--el-border-color-lighter);
+  flex-direction: column;
+}
+
+.device-panel__header {
+  display: flex;
+  align-items: center;
+  min-height: 54px;
+  padding: 10px 16px;
+  background: var(--el-bg-color);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.device-panel__header > div {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  width: 100%;
+  gap: 12px;
+}
+
+.device-panel__header strong {
+  font-size: 15px;
+  color: var(--el-text-color-primary);
+}
+
+.device-panel__header span {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+.device-list {
+  display: flex;
+  min-height: 0;
+  padding: 10px;
+  overflow-y: auto;
+  flex: 1;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.device-item {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  min-height: 58px;
+  padding: 9px 10px;
+  color: var(--el-text-color-regular);
+  text-align: left;
+  cursor: pointer;
+  background: transparent;
+  border: 1px solid transparent;
+  border-radius: 6px;
+  transition:
+    color 0.2s ease,
+    background-color 0.2s ease,
+    border-color 0.2s ease;
+  gap: 10px;
+}
+
+.device-item:hover {
+  background: var(--el-fill-color-light);
+}
+
+.device-item.is-active {
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border-color: var(--el-color-primary-light-7);
+}
+
+.device-item__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  font-size: 15px;
+  color: var(--el-text-color-secondary);
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+  flex: 0 0 auto;
+}
+
+.device-item.is-active .device-item__icon {
+  color: var(--el-color-primary);
+  border-color: var(--el-color-primary-light-7);
+}
+
+.device-item__content {
+  display: flex;
+  min-width: 0;
+  flex: 1;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.device-item__name {
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 500;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.device-item__progress {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+.device-item__arrow {
+  color: var(--el-text-color-placeholder);
+  flex: 0 0 auto;
+}
+
+.device-content {
+  min-width: 0;
+  min-height: 0;
+  overflow: auto;
+}
+
+.device-inspection-panel {
+  display: flex;
+  min-height: 100%;
+  flex-direction: column;
+}
+
+.device-summary {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 70px;
+  padding: 12px 18px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  gap: 16px;
+}
+
+.device-summary__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.device-summary__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 36px;
+  height: 36px;
+  margin-right: 12px;
+  font-size: 17px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border-radius: 7px;
+  flex: 0 0 auto;
+}
+
+.device-summary h2 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.device-summary p {
+  margin: 1px 0 0;
+  overflow: hidden;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.device-summary__progress {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
+
+.item-progress-strip {
+  display: flex;
+  padding: 12px 18px;
+  overflow-x: auto;
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  gap: 8px;
+}
+
+.progress-item {
+  display: flex;
+  max-width: 240px;
+  min-width: 150px;
+  padding: 7px 10px;
+  color: var(--el-text-color-secondary);
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+  align-items: center;
+  gap: 8px;
+}
+
+.progress-item.is-process {
+  color: var(--el-color-primary);
+  border-color: var(--el-color-primary-light-5);
+}
+
+.progress-item.is-finish {
+  color: var(--el-color-success);
+  border-color: var(--el-color-success-light-5);
+}
+
+.progress-item__index {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 22px;
+  height: 22px;
+  font-size: 12px;
+  color: #fff;
+  background: var(--el-text-color-placeholder);
+  border-radius: 50%;
+  flex: 0 0 auto;
+}
+
+.progress-item.is-process .progress-item__index {
+  background: var(--el-color-primary);
+}
+
+.progress-item.is-finish .progress-item__index {
+  background: var(--el-color-success);
+}
+
+.progress-item__name {
+  min-width: 0;
+  overflow: hidden;
+  font-size: 13px;
+  font-weight: 500;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.current-item-layout {
+  display: grid;
+  grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr);
+  padding: 18px;
+  align-items: start;
+  gap: 16px;
+}
+
+.entry-section,
+.standard-section {
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 7px;
+}
+
+.entry-section .inspection-form {
+  max-width: none;
+  padding: 18px 18px 0;
+}
+
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  column-gap: 20px;
+}
+
+.form-grid :deep(.el-form-item) {
+  min-width: 0;
+  align-items: flex-start;
+}
+
+.form-grid :deep(.el-form-item__label) {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  height: 32px;
+  padding-right: 12px;
+  line-height: 32px;
+  white-space: nowrap;
+}
+
+.form-grid :deep(.el-form-item__content) {
+  min-width: 0;
+}
+
+.form-grid__wide {
+  grid-column: 1 / -1;
+}
+
+.standard-section {
+  background: var(--el-fill-color-extra-light);
+}
+
+.standard-section__header {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 14px;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-bg-color);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  gap: 8px;
+}
+
+.standard-section__header > :first-child {
+  color: var(--el-color-primary);
+}
+
+.standard-section__content {
+  max-height: 300px;
+  padding: 14px;
+  overflow-y: auto;
+  font-size: 14px;
+  line-height: 1.8;
+  color: var(--el-text-color-regular);
+  white-space: pre-wrap;
+}
+
+.standard-section__footer {
+  display: flex;
+  align-items: center;
+  min-height: 42px;
+  padding: 6px 14px;
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  gap: 12px;
+}
+
+.device-inspection-panel > .navigation-controls {
+  position: static;
+  min-height: 58px;
+  padding: 10px 18px;
+  margin-top: 0;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
 </style>

+ 312 - 250
src/views/pms/inspect/order/index.vue

@@ -1,236 +1,191 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm px-2"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('bomList.name')" prop="inspectOrderTitle">
-            <el-input
-              v-model="queryParams.inspectOrderTitle"
-              :placeholder="t('bomList.nHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-180px"
-            />
-          </el-form-item>
-          <!--          <el-form-item label="工单编码" prop="inspectOrderCode">-->
-          <!--            <el-input-->
-          <!--              v-model="queryParams.inspectOrderCode"-->
-          <!--              placeholder="请输入工单编码"-->
-          <!--              clearable-->
-          <!--              @keyup.enter="handleQuery"-->
-          <!--              class="!w-240px"-->
-          <!--            />-->
-          <!--          </el-form-item>-->
-          <el-form-item :label="t('operationFill.status')" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              :placeholder="t('operationFill.status')"
-              clearable
-              class="!w-180px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_ORDER_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </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-180px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
-            >
-            <!--          <el-button-->
-            <!--            type="primary"-->
-            <!--            plain-->
-            <!--            @click="openForm('create')"-->
-            <!--            v-hasPermi="['rq:iot-inspect-order:create']"-->
-            <!--          >-->
-            <!--            <Icon icon="ep:plus" class="mr-5px" /> 新增-->
-            <!--          </el-button>-->
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-inspect-order:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="inspect-order-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      :top-id="rootDeptId"
+      :deptId="currentDeptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="inspect-order-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="88px"
+      class="inspect-order-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('bomList.name')" prop="inspectOrderTitle">
+          <el-input
+            v-model="queryParams.inspectOrderTitle"
+            :placeholder="t('bomList.nHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('operationFill.status')" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            :placeholder="t('operationFill.status')"
+            clearable
+            class="query-control">
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_ORDER_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            :start-placeholder="t('info.start')"
+            :end-placeholder="t('info.end')"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="query-control query-control--date" />
+        </el-form-item>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list" height="calc(85vh - 138px)">
-          <zm-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('bomList.name')" align="center" prop="inspectOrderTitle" />
-          <!--          <el-table-column label="工单编码" align="center" prop="inspectOrderCode" />-->
-          <zm-table-column
-            :label="t('route.orderType')"
-            align="center"
-            prop="type"
-            min-width="90"
-          />
-          <zm-table-column
-            :label="t('operationFill.status')"
-            align="center"
-            prop="status"
-            min-width="95"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_INSPECT_ORDER_STATUS" :value="scope.row.status" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('iotMaintain.PersonInCharge')"
-            align="center"
-            prop="chargeName"
-            min-width="110"
-          />
-          <zm-table-column
-            :label="t('inspect.deviceCount')"
-            align="center"
-            prop="deviceCount"
-            min-width="100"
-          >
-            <template #default="scope">
-              <el-tag type="info"> {{ scope.row.deviceCount }}</el-tag>
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('inspect.needDevice')"
-            align="center"
-            prop="needDevice"
-            min-width="100"
-          >
-            <template #default="scope">
-              <el-tag type="success"> {{ scope.row.needDevice }}</el-tag>
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('inspect.exceptionCount')"
-            align="center"
-            prop="exceptionCount"
-            min-width="100"
-          >
-            <template #default="scope">
-              <el-tag type="danger"> {{ scope.row.exceptionCount }}</el-tag>
-            </template>
-          </zm-table-column>
-          <!--          <el-table-column label="备注" align="center" prop="remark" />-->
-          <zm-table-column
-            :label="t('inspect.generateTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('inspect.executeTime')"
-            align="center"
-            prop="executeDate"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('iotMaintain.operation')"
-            align="center"
-            min-width="160px"
-            fixed="right"
-            action
-          >
-            <template #default="scope">
-              <el-button link type="primary" @click="openForm(scope.row.id)">
-                {{ t('operationFill.view') }}
-              </el-button>
-              <el-button
-                v-if="scope.row.status === 'todo'"
-                link
-                type="primary"
-                @click="openWrite(scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-order:update']"
-              >
-                {{ t('operationFill.fill') }}
-              </el-button>
-              <el-button
-                link
-                type="warning"
-                v-if="scope.row.status === 'todo'"
-                @click="openDialog(scope.row.id)"
-              >
-                {{ t('inspect.ignore') }}
-              </el-button>
-              <!--              <el-button link type="primary" @click="openForm(scope.row.id)"> {{ t('inspect.ignore') }} </el-button>-->
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <el-dialog
-          v-model="dialogVisible"
-          title="忽略理由"
-          :width="600"
-          :before-close="handleClose"
-          append-to-body
-          :close-on-click-modal="false"
-        >
-          <el-form ref="reasonFormRef" :model="form" :rules="rules" label-width="60px">
-            <el-form-item label="理由" prop="reason">
-              <el-input
-                type="textarea"
-                v-model="form.reason"
-                placeholder="请输入忽略理由"
-                :rows="4"
-                resize="none"
-              />
-            </el-form-item>
-          </el-form>
-
-          <template #footer>
-            <el-button @click="handleCancel">取消</el-button>
-            <el-button type="primary" @click="handleConfirm">确定</el-button>
+      <el-form-item class="query-actions">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('operationFill.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('operationFill.reset') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-inspect-order:export']">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div
+      class="table-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+              settings-cache-key="pms-inspect-order-list">
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn :label="t('bomList.name')" prop="inspectOrderTitle" fixed="left" />
+              <ZmTableColumn :label="t('route.orderType')" prop="type" />
+              <ZmTableColumn :label="t('operationFill.status')" prop="status">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.PMS_INSPECT_ORDER_STATUS" :value="scope.row.status" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('iotMaintain.PersonInCharge')" prop="chargeName" />
+              <ZmTableColumn :label="t('inspect.deviceCount')" prop="deviceCount">
+                <template #default="scope">
+                  <el-tag type="info">{{ scope.row.deviceCount }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('inspect.needDevice')" prop="needDevice">
+                <template #default="scope">
+                  <el-tag type="success">{{ scope.row.needDevice }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('inspect.exceptionCount')" prop="exceptionCount">
+                <template #default="scope">
+                  <el-tag type="danger">{{ scope.row.exceptionCount }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('inspect.generateTime')"
+                prop="createTime"
+                :formatter="dateFormatter" />
+              <ZmTableColumn
+                :label="t('inspect.executeTime')"
+                prop="executeDate"
+                :formatter="dateFormatter" />
+              <ZmTableColumn :label="t('iotMaintain.operation')" :width="180" fixed="right" action>
+                <template #default="scope">
+                  <el-button link type="primary" @click="openForm(scope.row.id)">
+                    {{ t('operationFill.view') }}
+                  </el-button>
+                  <el-button
+                    v-if="scope.row.status === 'todo'"
+                    link
+                    type="primary"
+                    @click="openWrite(scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-order:update']">
+                    {{ t('operationFill.fill') }}
+                  </el-button>
+                  <el-button
+                    v-if="scope.row.status === 'todo'"
+                    link
+                    type="warning"
+                    @click="openDialog(scope.row.id)">
+                    {{ t('inspect.ignore') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
           </template>
-        </el-dialog>
-        <!-- 分页 -->
-        <Pagination
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <!-- 表单弹窗:添加/修改 -->
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
+  <el-dialog
+    v-model="dialogVisible"
+    title="忽略理由"
+    width="min(520px, 92vw)"
+    :before-close="handleClose"
+    append-to-body
+    :close-on-click-modal="false">
+    <el-form ref="reasonFormRef" :model="form" :rules="rules" size="default" label-width="60px">
+      <el-form-item label="理由" prop="reason">
+        <el-input
+          v-model="form.reason"
+          type="textarea"
+          placeholder="请输入忽略理由"
+          :rows="4"
+          resize="none" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button size="default" @click="handleCancel">取消</el-button>
+      <el-button size="default" type="primary" @click="handleConfirm">确定</el-button>
+    </template>
+  </el-dialog>
+
   <IotInspectOrderForm ref="formRef" @success="getList" />
 </template>
 
@@ -240,10 +195,10 @@ import download from '@/utils/download'
 import { IotInspectOrderApi, IotInspectOrderVO } from '@/api/pms/inspect/order'
 import IotInspectOrderForm from './IotInspectOrderForm.vue'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+import { useUserStore } from '@/store/modules/user'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotInspectOrderVO>()
 
 const { push } = useRouter()
 const { params } = useRoute()
@@ -254,11 +209,11 @@ const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const list = ref<IotInspectOrderVO[]>([]) // 列表的数据
-const status = params.status
-const deptId = params.deptId
+const rootDeptId = 156
+const currentDeptId = useUserStore().getUser.deptId || rootDeptId
+const routeDeptId = params.deptId
 const createTime = params.createTime
 const total = ref(0) // 列表的总页数
-let isLeftContentCollapsed = ref(false)
 const dialogVisible = ref(false)
 const queryParams = reactive({
   pageNo: 1,
@@ -275,6 +230,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 /** 查询列表 */
@@ -297,10 +253,20 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
+const handleSizeChange = (pageSize: number) => {
+  queryParams.pageSize = pageSize
+  handleQuery()
+}
+
+const handleCurrentChange = (pageNo: number) => {
+  queryParams.pageNo = pageNo
+  getList()
+}
+
 const reasonFormRef = ref(null)
 const form = reactive({
   id: undefined,
@@ -396,12 +362,11 @@ const handleExport = async () => {
 
 /** 初始化 **/
 onMounted(() => {
-  debugger
   if (params.status) {
     queryParams.status = params.status
   }
-  if (deptId != null) {
-    queryParams.deptId = deptId
+  if (routeDeptId != null) {
+    queryParams.deptId = routeDeptId
   }
 
   if (createTime) {
@@ -410,24 +375,121 @@ onMounted(() => {
   getList()
 })
 </script>
-<style scoped scss>
-.reason-confirm-container {
-  margin: 20px;
+<style scoped>
+@media (width >= 1800px) {
+  .inspect-order-page .inspect-order-query,
+  .inspect-order-page .query-row {
+    flex-wrap: nowrap;
+  }
 }
 
-/* 调整文本框样式 */
-:deep(.el-textarea__inner) {
-  font-size: 14px;
+@media (width <= 1500px) {
+  .inspect-order-page .inspect-order-query,
+  .inspect-order-page .query-row {
+    gap: 12px 18px;
+  }
+
+  .inspect-order-page .query-control {
+    width: 175px;
+  }
+
+  .inspect-order-page .query-control--date {
+    width: 220px;
+  }
 }
 
-/* 调整表单项间距 */
-:deep(.el-form-item) {
-  margin-bottom ::v-deep .el-table__header-wrapper {
-    position: sticky !important;
+@media (width <= 1200px) {
+  .inspect-order-page {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  .inspect-order-page :deep(.inspect-order-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .inspect-order-page .query-actions {
     width: 100%;
-    top: 0px;
-    z-index: 2000;
   }
-  :15px;
+}
+
+@media (width <= 768px) {
+  .inspect-order-page .inspect-order-query,
+  .inspect-order-page .table-panel {
+    padding: 12px;
+  }
+
+  .inspect-order-page .query-row,
+  .inspect-order-page .query-row :deep(.el-form-item),
+  .inspect-order-page .query-actions,
+  .inspect-order-page .query-control,
+  .inspect-order-page .query-control--date {
+    width: 100%;
+  }
+
+  .inspect-order-page .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    width: 100%;
+    gap: 8px;
+  }
+
+  .inspect-order-page .query-actions :deep(.el-button) {
+    width: 100%;
+  }
+}
+
+.inspect-order-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  min-width: 0;
+  gap: 12px 24px;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+.query-control--date {
+  width: 240px;
+}
+
+.inspect-order-query :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.inspect-order-query :deep(.el-form-item__label) {
+  white-space: nowrap;
 }
 </style>

+ 349 - 217
src/views/pms/inspect/plan/InspectRouteList.vue

@@ -1,165 +1,172 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="t('route.InspectionRoute')" style="width: 1300px; min-height: 600px">
-    <ContentWrap>
+  <Dialog
+    v-model="dialogVisible"
+    :title="t('route.SelectInspectionDevice')"
+    width="min(1280px, 94vw)"
+    class="inspect-route-select-dialog"
+    @close="handleClose">
+    <template #title>
+      <div class="dialog-title mb-4">
+        <div class="dialog-title__icon"><Icon icon="ep:guide" /></div>
+        <div>
+          <div class="dialog-title__text">{{ t('route.SelectInspectionDevice') }}</div>
+          <div class="dialog-title__subtitle">筛选并选择需要加入计划的巡检设备路线</div>
+        </div>
+      </div>
+    </template>
+
+    <div class="dialog-content">
       <el-form
-        class="-mb-15px"
-        :model="queryParams"
         ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-      >
-        <el-form-item :label="t('monitor.deviceName')" prop="deviceName" style="margin-left: 25px">
-          <el-input
-            v-model="queryParams.deviceName"
-            :placeholder="t('iotDevice.nameHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-220px"
-          />
-        </el-form-item>
-        <el-form-item  :label="t('iotDevice.code')" prop="deviceCode">
-          <el-input
-            v-model="queryParams.deviceCode"
-            :placeholder="t('iotDevice.codeHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-220px"
-          />
-        </el-form-item>
-        <el-form-item  :label="t('route.RouteName')" prop="routeName">
-          <el-input
-            v-model="queryParams.routeName"
-            :placeholder="t('route.nHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-220px"
-          />
-        </el-form-item>
-        <el-form-item :label="t('iotDevice.dept')" prop="deptId">
-          <el-tree-select
-            v-model="queryParams.deptId"
-            :data="deptList"
-            :props="defaultProps"
-            check-strictly
-            node-key="id"
-            filterable
-            placeholder="请选择部门"
-            class="!w-220px"
-          />
-          <!--              <el-tree-select-->
-          <!--                v-model="formData.deptId"-->
-          <!--                :data="deptList"-->
-          <!--                :props="defaultProps"-->
-          <!--                check-strictly-->
-          <!--                node-key="id"-->
-          <!--                placeholder="请选择归属部门"-->
-          <!--              />-->
-        </el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('chooseMaintain.search') }}</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('chooseMaintain.reset')  }}</el-button>
-          <el-button @click="handleConfirm" type="danger"
-            ><Icon icon="ep:check" class="mr-5px" />  {{ t('chooseMaintain.confirm')  }}</el-button
-          >
-<!--          <el-button @click="toggleAll" type="primary"-->
-<!--            ><Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.selectAll') }}</el-button-->
-<!--          >-->
-<!--          <el-button @click="invertSelection" type="info"-->
-<!--            ><Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.cancelAllSelection') }}</el-button-->
-<!--          >-->
+        :model="queryParams"
+        size="default"
+        label-width="84px"
+        class="query-panel">
+        <div class="query-row">
+          <el-form-item :label="t('monitor.deviceName')" prop="deviceName">
+            <el-input
+              v-model="queryParams.deviceName"
+              :placeholder="t('iotDevice.nameHolder')"
+              clearable
+              class="query-control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
+            <el-input
+              v-model="queryParams.deviceCode"
+              :placeholder="t('iotDevice.codeHolder')"
+              clearable
+              class="query-control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item :label="t('route.RouteName')" prop="routeName">
+            <el-input
+              v-model="queryParams.routeName"
+              :placeholder="t('route.nHolder')"
+              clearable
+              class="query-control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item :label="t('iotDevice.dept')" prop="deptId">
+            <el-tree-select
+              v-model="queryParams.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              filterable
+              clearable
+              placeholder="请选择部门"
+              class="query-control" />
+          </el-form-item>
+        </div>
+        <el-form-item class="query-actions">
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />{{ t('chooseMaintain.search') }}
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.reset') }}
+          </el-button>
         </el-form-item>
       </el-form>
-    </ContentWrap>
-    <!-- 新增:已选物料标签区域 -->
-    <ContentWrap v-if="selectedRows.length > 0"
-                 style="margin: -10px 0px 10px; padding: 10px 15px; background: #f8fafc; border-radius: 4px; border: 1px solid #ebeef5;">
-      <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
-        <div style="font-weight: bold; color: #606266; margin-right: 10px;">已选路线设备:</div>
-        <el-tag
-          v-for="item in selectedRows"
-          :key="item.id"
-          @close="removeTag(item)"
-          style="margin-bottom: 5px; position: relative; padding-right: 25px;"
-        >
-          {{ item.deviceCode }}-{{ item.routeName }}
-          <!-- 自定义关闭按钮 -->
-          <span class="close-icon" @click.stop="removeTag(item)">
-            <Icon icon="ep:close" style="font-size: 12px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);"/>
-          </span>
-        </el-tag>
+
+      <div v-if="selectedRows.length > 0" class="selected-panel">
+        <div class="selected-panel__label">已选 {{ selectedRows.length }} 条路线</div>
+        <div class="selected-panel__tags">
+          <el-tag
+            v-for="item in selectedRows"
+            :key="`${item.id}-${item.deviceCode || ''}`"
+            closable
+            @close="removeTag(item)">
+            {{ item.deviceCode }} - {{ item.routeName }}
+          </el-tag>
+        </div>
+      </div>
+
+      <section class="table-section">
+        <ZmTable
+          :loading="loading"
+          :data="list"
+          :max-height="380"
+          show-border
+          settings-cache-key="pms-inspect-plan-route-select"
+          @row-click="selectRow">
+          <ZmTableColumn :label="t('workOrderMaterial.select')" :width="64" hide-in-column-settings>
+            <template #default="{ row }">
+              <el-checkbox
+                :model-value="isSelected(row)"
+                class="no-label-checkbox"
+                @click.stop="selectRow(row)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn :label="t('iotDevice.code')" prop="deviceCode" />
+          <ZmTableColumn :label="t('iotDevice.name')" prop="deviceName" />
+          <ZmTableColumn :label="t('route.RouteName')" prop="routeName" />
+          <ZmTableColumn :label="t('iotDevice.dept')" prop="deptName" />
+          <ZmTableColumn
+            :label="t('common.createTime')"
+            prop="createTime"
+            :formatter="dateFormatter" />
+          <ZmTableColumn :label="t('operationFill.operation')" hide-in-column-settings>
+            <template #default="scope">
+              <el-button link type="primary" @click.stop="viewRoute(scope.row.itemJson)">
+                <Icon icon="ep:view" class="mr-5px" />{{ t('operationFill.view') }}
+              </el-button>
+            </template>
+          </ZmTableColumn>
+        </ZmTable>
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList" />
+      </section>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <span class="selection-summary">已选择 {{ selectedRows.length }} 条路线</span>
+        <div>
+          <el-button size="default" @click="dialogVisible = false">取消</el-button>
+          <el-button size="default" type="primary" @click="handleConfirm">
+            <Icon icon="ep:check" class="mr-5px" />{{ t('chooseMaintain.confirm') }}
+          </el-button>
+        </div>
       </div>
-    </ContentWrap>
-    <ContentWrap>
-      <el-table
-        ref="tableRef"
-        v-loading="loading"
-        :data="list"
-        :stripe="true"
-        :row-key="rowKey"
-        :show-overflow-tooltip="true"
-        table-layout="auto"
-        @row-click="handleRowClick"
-        class="full-width-table" >
-        <el-table-column width="80" :label="t('workOrderMaterial.select')">
-          <template #default="{ row }">
-            <el-checkbox
-              :model-value="selectedRows.some(item => item.id === row.id&&item.deviceCode===row.deviceCode)"
-              @click.stop="selectRow(row)"
-              class="no-label-radio"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />
-        <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" />
-        <el-table-column :label="t('route.RouteName')" align="center" prop="routeName" />
-        <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" />
-        <el-table-column
-          :label="t('common.createTime')"
-          align="center"
-          prop="createTime"
-          width="180"
-          :formatter="dateFormatter"
-        />
-        <el-table-column :label="t('operationFill.operation')" align="center">
-          <template #default="scope">
-            <el-button link type="primary" @click="viewRoute(scope.row.itemJson)">
-              {{ t('operationFill.view') }}
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <!-- 分页 -->
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-      <RouteInspectItemDrawer
-        ref="showDrawer"
-        :model-value="drawerVisible"
-        @update:model-value="(val) => (drawerVisible = val)"
-      />
-    </ContentWrap>
+    </template>
+
+    <RouteInspectItemDrawer
+      ref="showDrawer"
+      :model-value="drawerVisible"
+      @update:model-value="(val) => (drawerVisible = val)" />
   </Dialog>
 </template>
 
 <script setup lang="ts">
 import { IotDeviceVO } from '@/api/pms/device'
 import { dateFormatter } from '@/utils/formatTime'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import { IotInspectRouteApi } from '@/api/pms/inspect/route'
-import { IotInspectItemVO } from '@/api/pms/inspect/item'
-import RouteInspectItemDrawer from "@/views/pms/inspect/plan/RouteInspectItemDrawer.vue";
-import {ref} from "vue";
-import {defaultProps, handleTree} from "@/utils/tree";
-import * as DeptApi from "@/api/system/dept";
+import RouteInspectItemDrawer from '@/views/pms/inspect/plan/RouteInspectItemDrawer.vue'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+type InspectRouteDeviceRow = IotDeviceVO & {
+  routeName: string
+  itemJson: string
+  createTime: string
+  deviceId: number
+  item?: string
+  standard?: string
+}
+
+const { ZmTable, ZmTableColumn } = useTableComponents<InspectRouteDeviceRow>()
 const { t } = useI18n() // 国际化
 const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
 const dialogVisible = ref(false) // 弹窗的是否展示
 const loading = ref(true) // 列表的加载中
 const queryFormRef = ref() // 搜索的表单
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
+const list = ref<InspectRouteDeviceRow[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const deptList = ref<Tree[]>([]) // 树形结构
 const drawerVisible = ref<boolean>(false)
@@ -173,38 +180,34 @@ const queryParams = reactive({
   assetClass: undefined,
   deviceName: undefined,
   deviceCode: undefined,
-  routeName: undefined,
+  routeName: undefined
 })
-// 生成行唯一标识
-const rowKey = (row: any) => {
-  return `${row.id}`; // 确保行更新时重新渲染
-};
-// 处理单选逻辑
-const selectRow = (row) => {
-  const index = selectedRows.value.findIndex(item => item.id === row.id&&item.deviceCode===row.deviceCode);
+
+const selectedRows = ref<InspectRouteDeviceRow[]>([])
+
+const isSelected = (row: InspectRouteDeviceRow) =>
+  selectedRows.value.some((item) => item.id === row.id && item.deviceCode === row.deviceCode)
+
+const selectRow = (row: InspectRouteDeviceRow) => {
+  const index = selectedRows.value.findIndex(
+    (item) => item.id === row.id && item.deviceCode === row.deviceCode
+  )
   if (index > -1) {
-    selectedRows.value.splice(index, 1); // 取消选中
-    // row.quantity = undefined;
+    selectedRows.value.splice(index, 1)
   } else {
-    selectedRows.value.push({ ...row }); // 选中
+    selectedRows.value.push({ ...row })
   }
-};
-// 新增:移除标签方法
-const removeTag = (route) => {
-  // 从已选列表中移除
-  const index = selectedRows.value.findIndex(item => item.id === route.id&&item.deviceCode === route.deviceCode);
-  debugger
+}
+
+const removeTag = (route: InspectRouteDeviceRow) => {
+  const index = selectedRows.value.findIndex(
+    (item) => item.id === route.id && item.deviceCode === route.deviceCode
+  )
   if (index !== -1) {
-    selectedRows.value.splice(index, 1);
+    selectedRows.value.splice(index, 1)
   }
+}
 
-  // 如果当前行在当前页,取消其选中状态
-  const rowInTable = list.value.find(item => item.id === route.id&&item.deviceCode === route.deviceCode);
-  if (rowInTable) {
-    const rowIndex = list.value.indexOf(rowInTable);
-    // 这里可以添加逻辑强制更新行状态(如果需要)
-  }
-};
 const handleConfirm = () => {
   if (selectedRows.value.length === 0) {
     ElMessage.warning('请至少选择一个')
@@ -215,60 +218,22 @@ const handleConfirm = () => {
     'choose',
     selectedRows.value.map((row) => ({
       ...row,
-      // 确保返回必要字段
       code: row.item,
       name: row.standard
     }))
   )
   dialogVisible.value = false
-  handleClose()
 }
 
-// 关闭时清空选择
 const handleClose = () => {
-  tableRef.value?.clearSelection()
   selectedRows.value = []
   emit('close')
 }
 
-
 const viewRoute = (itemJson) => {
   drawerVisible.value = true
   showDrawer.value.openDrawer(JSON.parse(itemJson))
 }
-const toggleAll = () => {
-  list.value.forEach((row) => {
-    tableRef.value.toggleRowSelection(row, true)
-  })
-}
-
-const handleSelectionChange = (val) => {
-  selectedRows.value = val
-}
-const invertSelection = () => {
-  selectedRows.value.forEach((row) => {
-    tableRef.value.toggleRowSelection(row, false)
-    selectedRows.value.splice(row)
-  })
-}
-const selectedRows = ref<IotInspectItemVO[]>([]) // 多选数据(存储所有选中行的数组)
-const tableRef = ref()
-
-// 多选 切换行选中状态
-const toggleRow = (row) => {
-  const includes = selectedRows.value.includes(row)
-  if (includes) {
-    selectedRows.value.splice(row)
-  } else {
-    selectedRows.value.push(row) // 选中
-  }
-  tableRef.value.toggleRowSelection(row, !includes)
-}
-
-// 点击整行选中
-const handleRowClick = (row) => {
-  toggleRow(row)
-}
 
 const open = async (classify) => {
   dialogVisible.value = true
@@ -280,7 +245,7 @@ const open = async (classify) => {
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
 }
 defineExpose({ open })
-const { wsCache } = useCache()
+
 const getList = async () => {
   loading.value = true
   list.value = []
@@ -298,21 +263,188 @@ const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
-const choose = (row: DictDataVO) => {
-  emit('choose', row)
-  dialogVisible.value = false
-}
+
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 </script>
-<style lang="scss">
-.no-label-radio .el-radio__label {
+<style lang="scss" scoped>
+@media (width <= 1400px) {
+  .inspect-route-select-dialog .query-row {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (width <= 768px) {
+  .inspect-route-select-dialog .dialog-title__subtitle {
+    display: none;
+  }
+
+  .inspect-route-select-dialog .query-panel,
+  .inspect-route-select-dialog .query-row,
+  .inspect-route-select-dialog .query-actions,
+  .inspect-route-select-dialog .query-control {
+    width: 100%;
+  }
+
+  .inspect-route-select-dialog .query-row {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .inspect-route-select-dialog .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    width: 100%;
+  }
+
+  .inspect-route-select-dialog .query-actions :deep(.el-button) {
+    width: 100%;
+  }
+
+  .inspect-route-select-dialog .selected-panel {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
+}
+
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.dialog-title__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding-top: 8px;
+}
+
+.query-panel {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  background: var(--el-fill-color-extra-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  gap: 12px 20px;
+}
+
+.query-row {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  min-width: 0;
+  flex: 1 1 880px;
+  gap: 12px 16px;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  gap: 8px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 100%;
+}
+
+.query-panel :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.query-panel :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+.selected-panel {
+  display: flex;
+  align-items: center;
+  padding: 10px 14px;
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-8);
+  border-radius: 8px;
+  gap: 12px;
+}
+
+.selected-panel__label {
+  font-weight: 600;
+  color: var(--el-color-primary);
+  white-space: nowrap;
+  flex: 0 0 auto;
+}
+
+.selected-panel__tags {
+  display: flex;
+  flex-wrap: wrap;
+  min-width: 0;
+  gap: 6px;
+  transform: translateY(1.5px);
+}
+
+.table-section {
+  padding: 12px 12px 0;
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.table-section :deep(.pagination-container) {
+  padding: 12px 8px;
+}
+
+.no-label-checkbox :deep(.el-checkbox__label) {
   display: none;
 }
-.no-label-radio .el-radio__inner {
-  margin-right: 0;
+
+.dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+}
+
+.selection-summary {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
 }
 </style>

+ 388 - 243
src/views/pms/inspect/plan/IotInspectPlan.vue

@@ -1,199 +1,139 @@
 <template>
-  <ContentWrap v-loading="formLoading">
-    <ContentWrap>
-      <el-form
-        ref="formRef"
-        :model="formData"
-        :rules="formRules"
-        style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-        label-width="130px"
-      >
-        <div class="base-expandable-content">
-          <el-row>
-            <el-col :span="12">
-              <el-form-item :label="t('main.planName')" prop="planTitle">
-                <el-input v-model="formData.planTitle" :placeholder="t('main.nameHolder')" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item :label="t('workOrderMaterial.unit')" prop="planUnit">
-                <el-select
-                  v-model="formData.planUnit"
-                  :placeholder="t('route.unitHolder')"
-                  :disabled="formType === 'update'"
-                  clearable
-                >
-                  <el-option
-                    v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_UNIT)"
-                    :key="dict.label"
-                    :label="dict.label"
-                    :value="dict.value"
-                  />
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item :label="t('route.cycle')" prop="planCycle">
-                <el-input-number
-                  style="width: 100%"
-                  v-model="formData.planCycle"
-                  :precision="0"
-                  :min="1"
-                  :max="99999"
-                />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item :label="t('route.beginCreateTime')" prop="beginCreateTime">
-                <el-date-picker
-                  style="width: 150%"
-                  v-model="formData.beginCreateTime"
-                  type="datetime"
-                  value-format="x"
-                />
-              </el-form-item>
-            </el-col>
-            <el-col :span="24">
-              <el-form-item :label="t('form.remark')" prop="remark">
-                <el-input type="textarea" v-model="formData.remark" />
-              </el-form-item>
-            </el-col>
-          </el-row>
+  <div class="plan-form-page" v-loading="formLoading">
+    <header class="page-header">
+      <div class="page-header__main">
+        <div class="page-header__icon"><Icon icon="ep:calendar" /></div>
+        <div>
+          <h1>{{ pageTitle }}</h1>
+          <p>配置巡检周期、首次执行时间及设备路线顺序</p>
         </div>
-      </el-form>
-    </ContentWrap>
-    <ContentWrap>
-      <!-- 列表 -->
-<!--      <ContentWrap>-->
-        <ContentWrap>
-          <el-form
-            class="-mb-15px"
-            :model="queryParams"
-            ref="queryFormRef"
-            :inline="true"
-            label-width="68px"
-          >
-            <el-form-item>
-              <el-button @click="openForm" type="primary"
-                ><Icon icon="ep:plus" class="mr-5px" />
-                {{ t('route.SelectInspectionDevice') }}</el-button
-              >
-            </el-form-item>
-          </el-form>
-        </ContentWrap>
-      <draggable
-        v-model="list"
-        item-key="id"
-        tag="div"
-        class="sortable-container"
-        handle=".sortable-item"
-        :animation="150"
-        @start="dragStart"
-        @end="dragEnd"
-      >
-        <template #item="{ element, index }">
-          <div class="sortable-item">
-            <!-- 序号显示 -->
-            <div class="order-number">{{ index + 1 }}</div>
-
-            <!-- 拖动手柄 -->
-            <span class="drag-handle">≡</span>
-
-            <!-- 组件内容 -->
-            <div class="component-content">
-                <span style="font-weight: bold">{{ t('iotDevice.code') }}:</span
-                ><span style="font-size: 14px">{{ element.deviceCode }}</span
-            >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-              <span style="font-weight: bold">{{ t('iotDevice.name') }}:</span
-              ><span style="font-size: 14px">{{ element.deviceName }}</span>
-              &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-              <span style="font-weight: bold">{{ t('route.RouteName') }}:</span
-              ><span style="font-size: 14px">{{ element.routeName }}</span>
-            </div>
-            <div>
-<!--              <el-button type="warning" @click="deleteDraggable(index)">{{ t('form.delete') }}</el-button>-->
-              <el-button
-                style="vertical-align: middle"
-                link
-                type="warning"
-                @click="viewRoute(element.itemJson)"
-              >
-                <Icon style="vertical-align: middle; color: #c36e5b" icon="ep:view" />
-                {{ t('route.InspectionRoute') }}
-              </el-button>
-              <el-button
-                style="vertical-align: middle"
-                link
-                type="danger"
-                @click="handleDelete(element.id,element.deviceId)"
-              >
-                <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
-                {{ t('form.delete') }}
-              </el-button>
+      </div>
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </header>
+
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      size="default"
+      label-width="110px"
+      class="plan-form">
+      <section class="form-section">
+        <header class="section-heading"><span></span>计划信息</header>
+        <div class="form-grid">
+          <el-form-item :label="t('main.planName')" prop="planTitle">
+            <el-input v-model="formData.planTitle" :placeholder="t('main.nameHolder')" />
+          </el-form-item>
+          <el-form-item :label="t('workOrderMaterial.unit')" prop="planUnit">
+            <el-select
+              v-model="formData.planUnit"
+              :placeholder="t('route.unitHolder')"
+              :disabled="formType === 'update'"
+              clearable>
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_UNIT)"
+                :key="dict.label"
+                :label="dict.label"
+                :value="dict.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item :label="t('route.cycle')" prop="planCycle">
+            <el-input-number
+              v-model="formData.planCycle"
+              :precision="0"
+              :min="1"
+              :max="99999"
+              controls-position="right" />
+          </el-form-item>
+          <el-form-item :label="t('route.beginCreateTime')" prop="beginCreateTime">
+            <el-date-picker
+              v-model="formData.beginCreateTime"
+              type="datetime"
+              value-format="x"
+              placeholder="请选择首次执行时间" />
+          </el-form-item>
+          <el-form-item :label="t('form.remark')" prop="remark" class="form-grid__wide">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入备注" />
+          </el-form-item>
+        </div>
+      </section>
+
+      <section class="form-section route-section">
+        <header class="section-heading section-heading--actions">
+          <div class="section-heading__title"><span></span>巡检设备路线</div>
+          <el-button size="default" type="primary" @click="openForm">
+            <Icon icon="ep:plus" class="mr-5px" />
+            {{ t('route.SelectInspectionDevice') }}
+          </el-button>
+        </header>
+
+        <el-empty v-if="list.length === 0" description="暂未选择巡检设备路线" :image-size="72" />
+        <draggable
+          v-else
+          v-model="list"
+          item-key="id"
+          tag="div"
+          class="sortable-container"
+          handle=".sortable-item"
+          :animation="150"
+          @start="dragStart"
+          @end="dragEnd">
+          <template #item="{ element, index }">
+            <div class="sortable-item">
+              <div class="order-number">{{ index + 1 }}</div>
+              <span class="drag-handle"><Icon icon="ep:rank" /></span>
+              <div class="component-content">
+                <div class="item-field">
+                  <span class="item-field__label">{{ t('iotDevice.code') }}</span>
+                  <span class="item-field__value">{{ element.deviceCode || '-' }}</span>
+                </div>
+                <div class="item-field">
+                  <span class="item-field__label">{{ t('iotDevice.name') }}</span>
+                  <span class="item-field__value">{{ element.deviceName || '-' }}</span>
+                </div>
+                <div class="item-field">
+                  <span class="item-field__label">{{ t('route.RouteName') }}</span>
+                  <span class="item-field__value">{{ element.routeName || '-' }}</span>
+                </div>
+              </div>
+              <div class="item-actions">
+                <el-button link type="primary" @click="viewRoute(element.itemJson)">
+                  <Icon icon="ep:view" class="mr-5px" />{{ t('route.InspectionRoute') }}
+                </el-button>
+                <el-button link type="danger" @click="handleDelete(element.id, element.deviceId)">
+                  <Icon icon="ep:delete" class="mr-5px" />{{ t('form.delete') }}
+                </el-button>
+              </div>
             </div>
-          </div>
-        </template>
-      </draggable>
-<!--        <ContentWrap>-->
-<!--          <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">-->
-<!--            <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />-->
-<!--            <el-table-column :label="t('iotDevice.name')"  align="center" prop="deviceName" />-->
-<!--            <el-table-column :label="t('route.RouteName')" align="center" prop="routeName" />-->
-<!--            <el-table-column :label="t('operationFill.operation')" align="center" min-width="120px">-->
-<!--              <template #default="scope">-->
-<!--                <div-->
-<!--                  style="display: flex; justify-content: center; align-items: center; width: 100%"-->
-<!--                >-->
-<!--                  <div style="margin-right: 10px">-->
-<!--                    <el-button-->
-<!--                      style="vertical-align: middle"-->
-<!--                      link-->
-<!--                      type="warning"-->
-<!--                      @click="viewRoute(scope.row.itemJson)"-->
-<!--                    >-->
-<!--                      <Icon style="vertical-align: middle; color: #c36e5b" icon="ep:view" />-->
-<!--                      {{ t('route.InspectionRoute') }}-->
-<!--                    </el-button>-->
-<!--                  </div>-->
-<!--                  <div>-->
-<!--                    <el-button-->
-<!--                      style="vertical-align: middle"-->
-<!--                      link-->
-<!--                      type="danger"-->
-<!--                      @click="handleDelete(scope.row.id,scope.row.deviceId)"-->
-<!--                    >-->
-<!--                      <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />-->
-<!--                      {{ t('form.delete') }}-->
-<!--                    </el-button>-->
-<!--                  </div>-->
-<!--                </div>-->
-<!--              </template>-->
-<!--            </el-table-column>-->
-<!--          </el-table>-->
-<!--        </ContentWrap>-->
-<!--      </ContentWrap>-->
-    </ContentWrap>
-    <ContentWrap>
-      <el-form>
-        <el-form-item style="float: right">
-          <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('iotMaintain.save') }}</el-button>
-          <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
+          </template>
+        </draggable>
+      </section>
+    </el-form>
+
+    <footer class="action-bar">
+      <el-button size="default" @click="close">{{ t('iotMaintain.cancel') }}</el-button>
+      <el-button size="default" type="primary" :loading="formLoading" @click="submitForm">
+        <Icon icon="ep:check" class="mr-5px" />{{ t('iotMaintain.save') }}
+      </el-button>
+    </footer>
+
     <InspectItemList
       ref="inspectItemFormRef"
       :classify="formData.deviceClassify"
       :deviceId="formData.deviceId"
-      @choose="inspectItemChoose"
-    />
+      @choose="inspectItemChoose" />
     <InspectRouteList ref="deviceFormRef" @choose="deviceChoose" />
     <RouteInspectItemDrawer
       ref="showDrawer"
       :model-value="drawerVisible"
-      @update:model-value="(val) => (drawerVisible = val)"
-    />
-  </ContentWrap>
+      @update:model-value="(val) => (drawerVisible = val)" />
+  </div>
 </template>
 <script setup lang="ts">
 import * as UserApi from '@/api/system/user'
@@ -219,6 +159,7 @@ const { currentRoute, push } = useRouter()
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const pageTitle = computed(() => (formType.value === 'update' ? '编辑巡检计划' : '新增巡检计划'))
 const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
 const list = ref<IotInspectRouteVO[]>([]) // 列表的数据
@@ -252,7 +193,7 @@ const items = ref([])
 const deviceChoose = (rows) => {
   debugger
   rows.forEach((it) => {
-    const ifExist = list.value.find((item) => item.id === it.id&&item.deviceId===it.deviceId)
+    const ifExist = list.value.find((item) => item.id === it.id && item.deviceId === it.deviceId)
     if (!ifExist) {
       list.value.push(it)
       console.log(JSON.stringify(it))
@@ -317,9 +258,10 @@ const submitForm = async () => {
       //校验是否是已有计划
       const newitems = itemsWithIndex
       formData.value.deviceIds = JSON.stringify(newitems.value)
-      if (!formData.value.deptId) {formData.value.deptId = user.user.deptId}
-      await IotInspectPlanApi.createIotInspectPlan(data).then((res) => {
-      })
+      if (!formData.value.deptId) {
+        formData.value.deptId = user.user.deptId
+      }
+      await IotInspectPlanApi.createIotInspectPlan(data).then((res) => {})
       message.success(t('common.createSuccess'))
       close()
     } else {
@@ -358,7 +300,7 @@ onMounted(async () => {
 const handleDelete = async (id: number, deviceId: number) => {
   try {
     debugger
-    const index = list.value.findIndex((item) => item.id === id&&item.deviceId===deviceId)
+    const index = list.value.findIndex((item) => item.id === id && item.deviceId === deviceId)
     if (index !== -1) {
       // 通过 splice 删除元素
       list.value.splice(index, 1)
@@ -367,100 +309,303 @@ const handleDelete = async (id: number, deviceId: number) => {
 }
 </script>
 <style scoped>
-.base-expandable-content {
-  overflow: hidden; /* 隐藏溢出的内容 */
-  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+@media (width <= 1400px) {
+  .plan-form-page .form-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .plan-form-page .component-content {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
 }
-/* 横向布局容器 */
 
+@media (width <= 768px) {
+  .plan-form-page .page-header {
+    align-items: flex-start;
+  }
 
-/* 拖拽手柄样式 */
-.drag-handle {
-  opacity: 0.5;
-  cursor: move;
-  transition: opacity 0.3s;
+  .plan-form-page .page-header p {
+    display: none;
+  }
+
+  .plan-form-page .form-grid {
+    grid-template-columns: minmax(0, 1fr);
+    padding: 14px 14px 0;
+  }
+
+  .plan-form-page .form-grid__wide {
+    grid-column: auto;
+  }
+
+  .plan-form-page .section-heading--actions {
+    height: auto;
+    min-height: 44px;
+    padding-top: 6px;
+    padding-bottom: 6px;
+  }
+
+  .plan-form-page .sortable-item {
+    align-items: flex-start;
+  }
+
+  .plan-form-page .drag-handle {
+    padding-right: 8px;
+    padding-left: 8px;
+  }
+
+  .plan-form-page .component-content {
+    grid-template-columns: minmax(0, 1fr);
+    gap: 6px;
+  }
+
+  .plan-form-page .item-actions {
+    flex-direction: column;
+    align-items: flex-end;
+  }
+
+  .plan-form-page .item-actions :deep(.el-button + .el-button) {
+    margin-left: 0;
+  }
+}
+
+.plan-form-page {
+  display: flex;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  flex-direction: column;
+  gap: 12px;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 66px;
+  padding: 12px 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.page-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
 }
-.drag-handle:hover {
-  opacity: 1;
+
+.page-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
 }
 
-/* 拖拽时的悬停效果 */
-.horizontal-item:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+.page-header h1 {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
 }
 
-/* 滚动条样式 */
-.horizontal-list::-webkit-scrollbar {
-  height: 8px;
+.page-header p {
+  margin: 1px 0 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
 }
-.horizontal-list::-webkit-scrollbar-thumb {
-  background: #888;
-  border-radius: 4px;
+
+.plan-form {
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  flex-direction: column;
+  gap: 12px;
 }
 
-.sortable-container {
-  cursor: move;
+.form-section {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-lighter);
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-heading > span,
+.section-heading__title > span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.section-heading--actions {
+  justify-content: space-between;
+}
+
+.section-heading__title {
   display: flex;
+  align-items: center;
+}
+
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  column-gap: 24px;
+  padding: 18px 18px 2px;
+}
+
+.form-grid__wide {
+  grid-column: 1 / -1;
+}
+
+.plan-form :deep(.el-form-item) {
+  margin-bottom: 16px;
+}
+
+.plan-form :deep(.el-input),
+.plan-form :deep(.el-input-number),
+.plan-form :deep(.el-select),
+.plan-form :deep(.el-date-editor) {
+  width: 100%;
+}
+
+.route-section {
+  display: flex;
+  min-height: 280px;
+  flex: 1;
   flex-direction: column;
-  gap: 9px;
-  //max-height: 80vh;
+}
+
+.route-section :deep(.el-empty) {
+  flex: 1;
+}
+
+.sortable-container {
+  display: flex;
+  min-height: 0;
+  padding: 14px;
   overflow-y: auto;
-  padding: 7px;
+  cursor: default;
+  flex: 1;
+  flex-direction: column;
+  gap: 10px;
 }
 
 .sortable-item {
   display: flex;
+  min-height: 60px;
+  padding: 10px 12px;
+  cursor: grab;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
   align-items: center;
-  padding: 8px;
-  background: #fff;
-  border: 1px solid #ebeef5;
-  border-radius: 8px;
   transition:
-    transform 0.3s,
-    box-shadow 0.3s;
+    border-color 0.2s ease,
+    box-shadow 0.2s ease;
   user-select: none;
-  height: 50%;
 }
 
 .sortable-item:hover {
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  border-color: var(--el-color-primary-light-7);
+  box-shadow: 0 3px 10px rgb(0 0 0 / 6%);
+}
+
+.sortable-item:active {
+  cursor: grabbing;
 }
 
 .order-number {
-  width: 24px;
-  height: 24px;
   display: flex;
   align-items: center;
   justify-content: center;
-  background: #409eff;
-  color: white;
+  width: 26px;
+  height: 26px;
+  margin-right: 2px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #fff;
+  background: var(--el-color-primary);
   border-radius: 50%;
-  margin-right: 7px;
-  font-weight: bold;
+  flex: 0 0 auto;
 }
 
 .drag-handle {
-  padding: 0 12px;
-  opacity: 0.4;
-  transition: opacity 0.3s;
-}
-
-.drag-handle:hover {
-  opacity: 1;
+  display: flex;
+  padding: 8px 12px;
+  font-size: 18px;
+  color: var(--el-text-color-placeholder);
+  cursor: grab;
 }
 
 .component-content {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  min-width: 0;
   flex: 1;
+  gap: 16px;
+}
+
+.item-field {
+  display: flex;
   min-width: 0;
+  align-items: flex-start;
 }
 
-/* 优化滚动条 */
-.sortable-container::-webkit-scrollbar {
-  width: 8px;
+.item-field__label {
+  margin-right: 8px;
+  font-weight: 600;
+  color: var(--el-text-color-regular);
+  white-space: nowrap;
 }
-.sortable-container::-webkit-scrollbar-thumb {
-  background: #c0c4cc;
-  border-radius: 4px;
+
+.item-field__value {
+  min-width: 0;
+  color: var(--el-text-color-secondary);
+  overflow-wrap: anywhere;
+}
+
+.item-actions {
+  display: flex;
+  align-items: center;
+  margin-left: 16px;
+  flex: 0 0 auto;
+}
+
+.action-bar {
+  position: sticky;
+  bottom: 0;
+  z-index: 5;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  min-height: 56px;
+  padding: 10px 16px;
+  margin-top: auto;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 -4px 12px rgb(0 0 0 / 5%);
 }
 </style>

+ 276 - 226
src/views/pms/inspect/plan/index.vue

@@ -1,181 +1,164 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm px-2"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('main.planName')" prop="planTitle" style="margin-left: 25px">
-            <el-input
-              v-model="queryParams.planTitle"
-              :placeholder="t('main.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('main.planCode')" prop="planCode">
-            <el-input
-              v-model="queryParams.planCode"
-              :placeholder="t('main.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
-            <el-date-picker
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="daterange"
-              :start-placeholder="t('info.start')"
-              :end-placeholder="t('info.end')"
-              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-              class="!w-220px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-inspect-plan:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('action.add') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-inspect-plan:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="inspect-plan-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="inspect-plan-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="88px"
+      class="inspect-plan-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('main.planName')" prop="planTitle">
+          <el-input
+            v-model="queryParams.planTitle"
+            :placeholder="t('main.nameHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('main.planCode')" prop="planCode">
+          <el-input
+            v-model="queryParams.planCode"
+            :placeholder="t('main.codeHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            :start-placeholder="t('info.start')"
+            :end-placeholder="t('info.end')"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="query-control query-control--date" />
+        </el-form-item>
+      </div>
+
+      <el-form-item class="query-actions">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('operationFill.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('operationFill.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['rq:iot-inspect-plan:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('action.add') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-inspect-plan:export']">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div
+      class="table-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+              settings-cache-key="pms-inspect-plan-list">
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn :label="t('route.planTitle')" prop="planTitle" fixed="left" />
+              <ZmTableColumn :label="t('route.planNumber')" prop="planCode" />
+              <ZmTableColumn :label="t('route.cycle')" prop="planCycle" />
+              <ZmTableColumn :label="t('workOrderMaterial.unit')" prop="planUnit">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.PMS_INSPECT_UNIT" :value="scope.row.planUnit" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('bomList.status')" prop="status">
+                <template #default="scope">
+                  <el-switch
+                    v-model="scope.row.status"
+                    :active-value="0"
+                    :inactive-value="1"
+                    @change="handleStatusChange(scope.row)" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('route.beginCreateTime')"
+                prop="beginCreateTime"
+                :formatter="dateFormatter" />
+              <ZmTableColumn
+                :label="t('route.lastCreateTime')"
+                prop="lastCreateTime"
+                :formatter="dateFormatter" />
+              <ZmTableColumn :label="t('iotDevice.dept')" prop="deptName" />
+              <ZmTableColumn :label="t('inspect.createName')" prop="createName" />
+              <ZmTableColumn
+                :label="t('operationFill.operation')"
+                :width="120"
+                fixed="right"
+                action>
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-plan:update']">
+                    {{ t('fault.edit') }}
+                  </el-button>
+                  <el-button
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-plan:delete']">
+                    {{ t('fault.del') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list" height="63vh">
-          <zm-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('route.planTitle')" align="center" prop="planTitle" />
-          <zm-table-column
-            :label="t('route.planNumber')"
-            align="center"
-            prop="planCode"
-            min-width="200"
-          />
-          <zm-table-column
-            :label="t('route.cycle')"
-            align="center"
-            prop="planCycle"
-            min-width="80"
-          />
-          <zm-table-column
-            :label="t('workOrderMaterial.unit')"
-            align="center"
-            prop="planUnit"
-            min-width="80"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_INSPECT_UNIT" :value="scope.row.planUnit" />
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('bomList.status')" key="status" min-width="80">
-            <template #default="scope">
-              <el-switch
-                v-model="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="handleStatusChange(scope.row)"
-              />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('route.beginCreateTime')"
-            align="center"
-            prop="beginCreateTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('route.lastCreateTime')"
-            align="center"
-            prop="lastCreateTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('iotDevice.dept')"
-            align="center"
-            prop="deptName"
-            min-width="130"
-          />
-          <zm-table-column
-            :label="t('inspect.createName')"
-            align="center"
-            prop="createName"
-            min-width="130"
-          />
-          <zm-table-column
-            :label="t('operationFill.operation')"
-            align="center"
-            min-width="120"
-            fixed="right"
-            action
-          >
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-plan:update']"
-              >
-                {{ t('fault.edit') }}
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-plan:delete']"
-              >
-                {{ t('fault.del') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
-        <Pagination
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
   <!-- 表单弹窗:添加/修改 -->
   <IotInspectPlanForm ref="formRef" @success="getList" />
 </template>
@@ -188,18 +171,20 @@ import { IotInspectPlanApi, IotInspectPlanVO } from '@/api/pms/inspect/plan'
 import IotInspectPlanForm from './IotInspectPlanForm.vue'
 import { CommonStatusEnum } from '@/utils/constants'
 import { DICT_TYPE } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { useRouter } from 'vue-router'
 import { useI18n } from 'vue-i18n'
+import { useUserStore } from '@/store/modules/user'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotInspectPlanVO>()
 
 defineOptions({ name: 'IotInspectPlan' })
 const message = useMessage() // 消息弹窗
 
 const { push } = useRouter()
 const { t } = useI18n()
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 
 const loading = ref(true)
 const list = ref<IotInspectPlanVO[]>([])
@@ -220,13 +205,12 @@ const queryParams = reactive({
 const queryFormRef = ref()
 const exportLoading = ref(false)
 
-const isLeftContentCollapsed = ref(false)
-const hoverText = ref('')
 const selectedDept = ref<{ id: number; name: string }>()
 
-const handleDeptNodeClick = async (row) => {
+const handleDeptNodeClick = async (row: Tree) => {
   selectedDept.value = { id: row.id, name: row.name }
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 
@@ -265,11 +249,21 @@ const handleQuery = () => {
 }
 
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   selectedDept.value = undefined
   handleQuery()
 }
 
+const handleSizeChange = (pageSize: number) => {
+  queryParams.pageSize = pageSize
+  handleQuery()
+}
+
+const handleCurrentChange = (pageNo: number) => {
+  queryParams.pageNo = pageNo
+  getList()
+}
+
 const openForm = (type: string, id?: number) => {
   if (typeof id === 'number') {
     push({ name: 'InspectPlanEdit', params: { id } })
@@ -305,70 +299,126 @@ const handleExport = async () => {
   }
 }
 
-const toggleLeftContent = () => {
-  isLeftContentCollapsed.value = !isLeftContentCollapsed.value
-}
-
-const handleMouseOver = () => {
-  hoverText.value = isLeftContentCollapsed.value ? '展开' : '收起'
-}
-
-const handleMouseOut = () => {
-  hoverText.value = ''
-}
-
 onMounted(() => {
   getList()
 })
 </script>
 
 <style scoped>
-.leftcontent {
-  transition: width 0.3s ease;
-  position: relative;
+.inspect-plan-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
 }
 
-.leftcontent.collapsed {
-  width: 0;
-  overflow: hidden;
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  min-width: 0;
+  gap: 12px 24px;
 }
 
-.rightcontent {
-  position: relative;
+.query-actions {
+  flex: 0 0 auto;
 }
 
-.toggle-button {
-  position: absolute;
-  top: 44%;
-  transform: translate(-65%, -50%);
-  width: 12px;
-  height: 40px;
-  background-color: #f0f0f0;
-  cursor: pointer;
+.query-actions :deep(.el-form-item__content) {
   display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 1;
-  clip-path: polygon(0 0, 100% 18%, 100% 85%, 0 100%);
-  border-radius: 8px;
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
 }
 
-/* 添加鼠标悬停样式 */
-.toggle-button:hover {
-  background-color: #afafaf;
+.query-control--date {
+  width: 230px;
 }
 
-.triangle {
-  width: 0;
-  height: 0;
-  border-top: 4px solid transparent;
-  border-bottom: 4px solid transparent;
-  transition: transform 0.4s ease;
-  border-right: 5px solid gray; /* 修改为右边框显示颜色 */
-  border-left: none; /* 移除左边框 */
+:deep(.el-form-item) {
+  margin-bottom: 0;
 }
 
-.triangle.rotated {
-  transform: rotate(180deg);
+:deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+@media (width >= 1800px) {
+  .inspect-plan-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .inspect-plan-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 175px;
+  }
+
+  .query-control--date {
+    width: 220px;
+  }
+}
+
+@media (width <= 1200px) {
+  .inspect-plan-page {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  :deep(.inspect-plan-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .inspect-plan-query,
+  .table-panel {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions,
+  .query-control,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+  }
 }
 </style>

+ 306 - 169
src/views/pms/inspect/route/InspectItemList.vue

@@ -1,141 +1,141 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="t('route.selectInspectionItems')" style="width: 1300px; min-height: 50%">
-    <ContentWrap>
+  <Dialog
+    v-model="dialogVisible"
+    :title="t('route.selectInspectionItems')"
+    width="min(1200px, 92vw)"
+    class="inspect-item-select-dialog"
+    @close="handleClose">
+    <template #title>
+      <div class="dialog-title mb-4">
+        <div class="dialog-title__icon"><Icon icon="ep:finished" /></div>
+        <div>
+          <div class="dialog-title__text">{{ t('route.selectInspectionItems') }}</div>
+          <div class="dialog-title__subtitle">筛选并选择需要加入路线的巡检项目</div>
+        </div>
+      </div>
+    </template>
+
+    <div class="dialog-content">
       <el-form
-        class="-mb-15px"
-        :model="queryParams"
         ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-      >
-        <el-form-item :label="t('inspect.InspectionItems')" prop="item" style="margin-left: 45px">
-          <el-input
-            v-model="queryParams.item"
-            :placeholder="t('inspect.itemHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-200px"
-          />
-        </el-form-item>
-<!--        <el-form-item :label="t('iotDevice.brand')" prop="brandName">-->
-<!--          <el-input-->
-<!--            v-model="queryParams.brandName"-->
-<!--            :placeholder="t('iotDevice.brandHolder')"-->
-<!--            clearable-->
-<!--            @keyup.enter="handleQuery"-->
-<!--            class="!w-200px"-->
-<!--          />-->
-<!--        </el-form-item>-->
-        <el-form-item :label="t('common.createTime')" prop="createTime">
-          <el-date-picker
-            v-model="queryParams.createTime"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            type="daterange"
-            :start-placeholder="t('info.start')"
-            :end-placeholder="t('info.end')"
-            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-            class="!w-240px"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button>
-          <el-button @click="handleConfirm" type="danger"
-            ><Icon icon="ep:check" class="mr-5px" /> {{ t('workOrderMaterial.confirm') }}</el-button
-          >
-<!--          <el-button @click="toggleAll" type="primary"-->
-<!--            ><Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.selectAll') }}</el-button-->
-<!--          >-->
-<!--          <el-button @click="invertSelection" type="info"-->
-<!--            ><Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.cancelAllSelection') }}</el-button-->
-<!--          >-->
+        :model="queryParams"
+        size="default"
+        label-width="88px"
+        class="query-panel">
+        <div class="query-row">
+          <el-form-item :label="t('inspect.InspectionItems')" prop="item">
+            <el-input
+              v-model="queryParams.item"
+              :placeholder="t('inspect.itemHolder')"
+              clearable
+              class="query-control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item :label="t('common.createTime')" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              :start-placeholder="t('info.start')"
+              :end-placeholder="t('info.end')"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="query-control query-control--date" />
+          </el-form-item>
+        </div>
+        <el-form-item class="query-actions">
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />{{ t('operationFill.search') }}
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />{{ t('operationFill.reset') }}
+          </el-button>
         </el-form-item>
       </el-form>
-    </ContentWrap>
-    <!-- 新增:已选物料标签区域 -->
-    <ContentWrap v-if="selectedRows.length > 0"
-                 style="margin: -10px 0px 10px; padding: 10px 15px; background: #f8fafc; border-radius: 4px; border: 1px solid #ebeef5;">
-      <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
-        <div style="font-weight: bold; color: #606266; margin-right: 10px;">已选物料:</div>
-        <el-tag
-          v-for="item in selectedRows"
-          :key="item.id"
-          @close="removeTag(item)"
-          style="margin-bottom: 5px; position: relative; padding-right: 25px;"
-        >
-          {{ item.item }}
-          <!-- 自定义关闭按钮 -->
-          <span class="close-icon" @click.stop="removeTag(item)">
-            <Icon icon="ep:close" style="font-size: 12px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);"/>
-          </span>
-        </el-tag>
+
+      <div v-if="selectedRows.length > 0" class="selected-panel">
+        <div class="selected-panel__label">已选 {{ selectedRows.length }} 项</div>
+        <div class="selected-panel__tags">
+          <el-tag
+            v-for="item in selectedRows"
+            :key="`${item.id}-${item.deviceCode || ''}`"
+            closable
+            @close="removeTag(item)">
+            {{ item.item }}
+          </el-tag>
+        </div>
+      </div>
+
+      <section class="table-section">
+        <ZmTable
+          :loading="loading"
+          :data="list"
+          :max-height="360"
+          show-border
+          settings-cache-key="pms-inspect-route-item-select"
+          @row-click="handleRowClick">
+          <ZmTableColumn :label="t('workOrderMaterial.select')" :width="64" hide-in-column-settings>
+            <template #default="{ row }">
+              <el-checkbox
+                :model-value="isSelected(row)"
+                class="no-label-checkbox"
+                @click.stop="toggleRow(row)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn :label="t('inspect.InspectionItems')" prop="item" />
+          <ZmTableColumn :label="t('inspect.InspectionStandards')" prop="standard" />
+          <ZmTableColumn :label="t('iotDevice.brand')" prop="brandName" />
+          <ZmTableColumn :label="t('fileInfo.appendix')" prop="urls">
+            <template #default="scope">
+              <el-button
+                v-if="scope.row.urls"
+                link
+                type="primary"
+                @click.stop="openWeb(scope.row.urls)">
+                <Icon size="18" icon="ep:view" />
+              </el-button>
+              <span v-else>-</span>
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn :label="t('common.sort')" prop="sort" />
+          <ZmTableColumn
+            :label="t('common.createTime')"
+            prop="createTime"
+            :formatter="dateFormatter" />
+        </ZmTable>
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList" />
+      </section>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <span class="selection-summary">已选择 {{ selectedRows.length }} 项</span>
+        <div>
+          <el-button size="default" @click="dialogVisible = false">取消</el-button>
+          <el-button size="default" type="primary" @click="handleConfirm">
+            <Icon icon="ep:check" class="mr-5px" />{{ t('workOrderMaterial.confirm') }}
+          </el-button>
+        </div>
       </div>
-    </ContentWrap>
-    <ContentWrap>
-      <el-table
-        ref="tableRef"
-        v-loading="loading"
-        :data="list"
-        :stripe="true"
-        :show-overflow-tooltip="true"
-        @row-click="handleRowClick"
-        row-key="id"
-        @selection-change="handleSelectionChange"
-      >
-<!--        <el-table-column-->
-<!--          type="selection"-->
-<!--          :reserve-selection="true"-->
-<!--          :selectable="(row) => !row.disabled"-->
-<!--        />-->
-        <el-table-column width="80" :label="t('workOrderMaterial.select')">
-          <template #default="{ row }">
-            <el-checkbox
-              :model-value="selectedRows.some(item => item.id === row.id&&item.deviceCode===row.deviceCode)"
-              @click.stop="selectRow(row)"
-              class="no-label-radio"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('inspect.InspectionItems')" align="center" prop="item" />
-        <el-table-column :label="t('inspect.InspectionStandards')" align="center" prop="standard" />
-        <el-table-column :label="t('iotDevice.brand')" align="center" prop="brandName" />
-        <el-table-column :label="t('fileInfo.appendix')" align="center" prop="urls">
-          <template #default="scope">
-            <el-button v-if="scope.row.urls" link type="primary" @click="openWeb(scope.row.urls)">
-              <Icon size="19" icon="ep:view"
-            /></el-button>
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('common.sort')" align="center" prop="sort" />
-        <el-table-column
-          :label="t('common.createTime')"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-        />
-      </el-table>
-      <!-- 分页 -->
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </ContentWrap>
+    </template>
   </Dialog>
 </template>
 
 <script setup lang="ts">
-import { IotDeviceVO } from '@/api/pms/device'
 import { dateFormatter } from '@/utils/formatTime'
 import { useCache } from '@/hooks/web/useCache'
 import { IotInspectItemApi, IotInspectItemVO } from '@/api/pms/inspect/item'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+const { ZmTable, ZmTableColumn } = useTableComponents<IotInspectItemVO>()
 const { t } = useI18n() // 国际化
 const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
 const dialogVisible = ref(false) // 弹窗的是否展示
 const loading = ref(true) // 列表的加载中
 const queryFormRef = ref() // 搜索的表单
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
+const list = ref<IotInspectItemVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
@@ -143,7 +143,7 @@ const queryParams = reactive({
   item: undefined,
   deptId: undefined,
   deviceClassify: undefined,
-  brandName:undefined,
+  brandName: undefined,
   deviceId: undefined,
   createTime: undefined
 })
@@ -153,51 +153,28 @@ const openWeb = (url) => {
   )
 }
 const removeTag = (route) => {
-  // 从已选列表中移除
-  const index = selectedRows.value.findIndex(item => item.id === route.id);
-  debugger
+  const index = selectedRows.value.findIndex(
+    (item) => item.id === route.id && item.deviceCode === route.deviceCode
+  )
   if (index !== -1) {
-    selectedRows.value.splice(index, 1);
-  }
-
-  // 如果当前行在当前页,取消其选中状态
-  const rowInTable = list.value.find(item => item.id === route.id);
-  if (rowInTable) {
-    const rowIndex = list.value.indexOf(rowInTable);
-    // 这里可以添加逻辑强制更新行状态(如果需要)
+    selectedRows.value.splice(index, 1)
   }
-};
-const selectRow = (row) => {
-  selectedRow.value = selectedRow.value?.id === row.id ? null : row
-  emit('choose', row)
-  dialogVisible.value = false
-}
-const toggleAll = () => {
-  list.value.forEach((row) => {
-    tableRef.value.toggleRowSelection(row, true)
-  })
-}
-const handleSelectionChange = (val) => {
-  selectedRows.value = val
-}
-const invertSelection = () => {
-  selectedRows.value.forEach((row) => {
-    tableRef.value.toggleRowSelection(row, false)
-    selectedRows.value.splice(row)
-  })
 }
 const selectedRows = ref<IotInspectItemVO[]>([]) // 多选数据(存储所有选中行的数组)
-const tableRef = ref()
+
+const isSelected = (row: IotInspectItemVO) =>
+  selectedRows.value.some((item) => item.id === row.id && item.deviceCode === row.deviceCode)
 
 // 多选 切换行选中状态
-const toggleRow = (row) => {
-  const includes = selectedRows.value.includes(row)
-  if (includes) {
-    selectedRows.value.splice(row)
+const toggleRow = (row: IotInspectItemVO) => {
+  const index = selectedRows.value.findIndex(
+    (item) => item.id === row.id && item.deviceCode === row.deviceCode
+  )
+  if (index !== -1) {
+    selectedRows.value.splice(index, 1)
   } else {
-    selectedRows.value.push(row) // 选中
+    selectedRows.value.push(row)
   }
-  tableRef.value.toggleRowSelection(row, !includes)
 }
 
 // 点击整行选中
@@ -261,12 +238,10 @@ const handleConfirm = () => {
     }))
   )
   dialogVisible.value = false
-  handleClose()
 }
 
 // 关闭时清空选择
 const handleClose = () => {
-  tableRef.value?.clearSelection()
   selectedRows.value = []
   emit('close')
 }
@@ -275,21 +250,183 @@ const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
-const choose = (row: DictDataVO) => {
-  emit('choose', row)
-  dialogVisible.value = false
-}
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 </script>
-<style lang="scss">
-.no-label-radio .el-radio__label {
+<style lang="scss" scoped>
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.dialog-title__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding-top: 8px;
+}
+
+.query-panel {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  background: var(--el-fill-color-extra-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  gap: 12px 20px;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  min-width: 0;
+  gap: 12px 20px;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  gap: 8px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+.query-control--date {
+  width: 240px;
+}
+
+.query-panel :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.query-panel :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+.selected-panel {
+  display: flex;
+  align-items: center;
+  padding: 10px 14px;
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-8);
+  border-radius: 8px;
+  gap: 12px;
+}
+
+.selected-panel__label {
+  flex: 0 0 auto;
+  font-weight: 600;
+  color: var(--el-color-primary);
+  white-space: nowrap;
+}
+
+.selected-panel__tags {
+  display: flex;
+  flex-wrap: wrap;
+  min-width: 0;
+  gap: 6px;
+  transform: translateY(1.5px);
+}
+
+.table-section {
+  padding: 12px 12px 0;
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.table-section :deep(.pagination-container) {
+  padding: 12px 8px;
+}
+
+.no-label-checkbox :deep(.el-checkbox__label) {
   display: none;
 }
-.no-label-radio .el-radio__inner {
-  margin-right: 0;
+
+.dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+}
+
+.selection-summary {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+}
+
+@media (width <= 768px) {
+  .dialog-title__subtitle {
+    display: none;
+  }
+
+  .query-panel,
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions,
+  .query-control,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+  }
+
+  .selected-panel {
+    flex-direction: column;
+    gap: 8px;
+  }
 }
 </style>

+ 410 - 139
src/views/pms/inspect/route/IotInspectRoute.vue

@@ -1,90 +1,78 @@
 <template>
-  <ContentWrap v-loading="formLoading">
-    <ContentWrap>
-      <el-form
-        ref="formRef"
-        :model="formData"
-        :rules="formRules"
-        style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-        label-width="130px"
-      >
-        <div class="base-expandable-content">
-          <el-row>
-            <el-col :span="8">
-              <el-form-item :label="t('route.RouteName')" prop="routeName">
-                <el-input v-model="formData.routeName" :placeholder="t('route.RouteName')" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('monitor.category')" prop="deviceClassify">
-                <el-tree-select
-                  filterable
-                  clearable
-                  v-model="formData.deviceClassify"
-                  :data="productClassifyList"
-                  :props="defaultProps"
-                  check-strictly
-                  node-key="id"
-                  :placeholder="t('deviceForm.categoryHolder')"
-                />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('iotDevice.brand')" prop="brandName">
-                <el-select
-                  clearable
-                  v-model="formData.brandName"
-                  @clear="brandClear"
-                  :placeholder="t('iotDevice.brandHolder')"
-                  @click="openBrand"
-                />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item :label="t('faultForm.device')" prop="deviceName">
-                <el-input
-                  v-model="formData.deviceName"
-                  @clear="handleClear"
-                  :placeholder="t('iotMaintain.deviceHolder')"
-                  @click="openDevice"
-                  clearable
-                />
-              </el-form-item>
-            </el-col>
-            <el-col :span="16">
-              <el-form-item :label="t('inspect.routeDesc')" prop="routeDesc">
-                <el-input
-                  type="textarea"
-                  v-model="formData.routeDesc"
-                  :placeholder="t('inspect.routeDescHolder')"
-                  clearable
-                />
-              </el-form-item>
-            </el-col>
-          </el-row>
+  <div class="route-form-page" v-loading="formLoading">
+    <header class="page-header">
+      <div class="page-header__main">
+        <div class="page-header__icon"><Icon icon="ep:guide" /></div>
+        <div>
+          <h1>{{ pageTitle }}</h1>
+          <p>配置巡检设备、路线说明及巡检项执行顺序</p>
         </div>
-      </el-form>
-    </ContentWrap>
-    <ContentWrap>
-      <!-- 列表 -->
-      <ContentWrap>
-        <ContentWrap>
-          <el-form
-            class="-mb-15px"
-            :model="queryParams"
-            ref="queryFormRef"
-            :inline="true"
-            label-width="68px"
-          >
-            <el-form-item>
-              <el-button @click="openForm" type="primary"
-                ><Icon icon="ep:plus" class="mr-5px" />
-                {{ t('route.selectInspectionItems') }}</el-button
-              >
-            </el-form-item>
-          </el-form>
-        </ContentWrap>
+      </div>
+      <el-button @click="close"> <Icon icon="ep:arrow-left" class="mr-5px" />返回 </el-button>
+    </header>
+
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      size="default"
+      label-width="100px"
+      class="route-form">
+      <section class="form-section">
+        <header class="section-heading"><span></span>路线信息</header>
+        <div class="form-grid">
+          <el-form-item :label="t('route.RouteName')" prop="routeName">
+            <el-input v-model="formData.routeName" :placeholder="t('route.RouteName')" />
+          </el-form-item>
+          <el-form-item :label="t('monitor.category')" prop="deviceClassify">
+            <el-tree-select
+              v-model="formData.deviceClassify"
+              :data="productClassifyList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              :placeholder="t('deviceForm.categoryHolder')"
+              filterable
+              clearable />
+          </el-form-item>
+          <el-form-item :label="t('iotDevice.brand')" prop="brandName">
+            <el-select
+              v-model="formData.brandName"
+              :placeholder="t('iotDevice.brandHolder')"
+              clearable
+              @clear="brandClear"
+              @click="openBrand" />
+          </el-form-item>
+          <el-form-item :label="t('faultForm.device')" prop="deviceName">
+            <el-input
+              v-model="formData.deviceName"
+              :placeholder="t('iotMaintain.deviceHolder')"
+              clearable
+              @clear="handleClear"
+              @click="openDevice" />
+          </el-form-item>
+          <el-form-item :label="t('inspect.routeDesc')" prop="routeDesc" class="form-grid__wide">
+            <el-input
+              v-model="formData.routeDesc"
+              type="textarea"
+              :rows="3"
+              :placeholder="t('inspect.routeDescHolder')"
+              clearable />
+          </el-form-item>
+        </div>
+      </section>
+
+      <section class="form-section inspection-section">
+        <header class="section-heading section-heading--actions">
+          <div class="section-heading__title"><span></span>巡检项顺序</div>
+          <el-button type="primary" @click="openForm">
+            <Icon icon="ep:plus" class="mr-5px" />{{ t('route.selectInspectionItems') }}
+          </el-button>
+        </header>
+
+        <el-empty v-if="items.length === 0" description="暂未选择巡检项" :image-size="72" />
         <draggable
+          v-else
           v-model="items"
           item-key="id"
           tag="div"
@@ -92,47 +80,41 @@
           handle=".sortable-item"
           :animation="150"
           @start="dragStart"
-          @end="dragEnd"
-        >
+          @end="dragEnd">
           <template #item="{ element, index }">
             <div class="sortable-item">
-              <!-- 序号显示 -->
               <div class="order-number">{{ index + 1 }}</div>
-
-              <!-- 拖动手柄 -->
-              <span class="drag-handle">≡</span>
-
-              <!-- 组件内容 -->
+              <span class="drag-handle"><Icon icon="ep:rank" /></span>
               <div class="component-content">
-                <span style="font-weight: bold">{{ t('inspect.InspectionItems') }}:</span
-                ><span style="font-size: 14px">{{ element.item }}</span
-                >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-                <span style="font-weight: bold">{{ t('inspect.InspectionStandards') }}:</span
-                ><span style="font-size: 14px">{{ element.standard }}</span>
-              </div>
-              <div>
-                <el-button type="warning" @click="deleteDraggable(index)">{{ t('form.delete') }}</el-button>
+                <div class="item-field">
+                  <span class="item-field__label">{{ t('inspect.InspectionItems') }}</span>
+                  <span class="item-field__value">{{ element.item || '-' }}</span>
+                </div>
+                <div class="item-field item-field--standard">
+                  <span class="item-field__label">{{ t('inspect.InspectionStandards') }}</span>
+                  <span class="item-field__value">{{ element.standard || '-' }}</span>
+                </div>
               </div>
+              <el-button link type="danger" @click="deleteDraggable(index)">
+                <Icon icon="ep:delete" class="mr-5px" />{{ t('form.delete') }}
+              </el-button>
             </div>
           </template>
         </draggable>
-      </ContentWrap>
-    </ContentWrap>
-    <ContentWrap>
-      <el-form>
-        <el-form-item style="float: right">
-          <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('iotMaintain.save') }}</el-button>
-          <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
-    <InspectItemList
-      ref="inspectItemFormRef"
-      @choose="inspectItemChoose"
-    />
+      </section>
+    </el-form>
+
+    <footer class="action-bar">
+      <el-button size="default" @click="close">{{ t('iotMaintain.cancel') }}</el-button>
+      <el-button size="default" type="primary" :loading="formLoading" @click="submitForm">
+        <Icon icon="ep:check" class="mr-5px" />{{ t('iotMaintain.save') }}
+      </el-button>
+    </footer>
+
+    <InspectItemList ref="inspectItemFormRef" @choose="inspectItemChoose" />
     <DeviceList ref="deviceFormRef" @choose="deviceChoose" />
     <BrandList ref="brandFormRef" @choose="brandChoose" />
-  </ContentWrap>
+  </div>
 </template>
 <script setup lang="ts">
 import * as UserApi from '@/api/system/user'
@@ -146,7 +128,7 @@ import draggable from 'vuedraggable'
 import InspectItemList from '@/views/pms/inspect/route/InspectItemList.vue'
 import DeviceList from '@/views/pms/failure/DeviceList.vue'
 import { IotInspectRouteApi, IotInspectRouteVO } from '@/api/pms/inspect/route'
-import BrandList from "@/views/pms/device/BrandList.vue";
+import BrandList from '@/views/pms/device/BrandList.vue'
 
 /** 维修工单 表单 */
 defineOptions({ name: 'RouteAdd' })
@@ -159,6 +141,7 @@ const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const pageTitle = computed(() => (formType.value === 'update' ? '编辑巡检路线' : '新增巡检路线'))
 const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
@@ -188,7 +171,7 @@ const brandChoose = (row) => {
   // brandLabel.value = row.value
   formData.value.brandName = row.label
 }
-const brandClear = () =>{
+const brandClear = () => {
   formData.value.brand = undefined
   formData.value.brandName = undefined
 }
@@ -246,7 +229,11 @@ const openForm = () => {
     message.error('请选择设备类别')
     return
   }
-  inspectItemFormRef.value.open(formData.value.deviceClassify, formData.value.deviceId,formData.value.brandName)
+  inspectItemFormRef.value.open(
+    formData.value.deviceClassify,
+    formData.value.deviceId,
+    formData.value.brandName
+  )
 }
 const deviceFormRef = ref()
 const openDevice = () => {
@@ -263,7 +250,7 @@ const close = () => {
   // })
 
   delView(unref(currentRoute))
-  push({ name: 'IotInspectRoute', params:{}})
+  push({ name: 'IotInspectRoute', params: {} })
 }
 const itemsWithIndex = computed(() => {
   return items.value.map((item, index) => ({
@@ -275,7 +262,7 @@ const itemsWithIndex = computed(() => {
 })
 const itemsWithIndexUpdate = computed(() => {
   return items.value.map((item, index) => ({
-    itemId: item.itemId||item.id,
+    itemId: item.itemId || item.id,
     item: item.item,
     standard: item.standard,
     index: index + 1 // 序号从1开始
@@ -345,10 +332,57 @@ const handleDelete = async (id: number) => {
 }
 </script>
 <style scoped>
+@media (width <= 1400px) {
+  .route-form-page .form-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (width <= 768px) {
+  .route-form-page .page-header {
+    align-items: flex-start;
+  }
+
+  .route-form-page .page-header p {
+    display: none;
+  }
+
+  .route-form-page .form-grid {
+    grid-template-columns: minmax(0, 1fr);
+    padding: 14px 14px 0;
+  }
+
+  .route-form-page .form-grid__wide {
+    grid-column: auto;
+  }
+
+  .route-form-page .section-heading--actions {
+    height: auto;
+    min-height: 44px;
+    padding-top: 6px;
+    padding-bottom: 6px;
+  }
+
+  .route-form-page .component-content {
+    grid-template-columns: minmax(0, 1fr);
+    gap: 6px;
+  }
+
+  .route-form-page .sortable-item {
+    align-items: flex-start;
+  }
+
+  .route-form-page .drag-handle {
+    padding-right: 8px;
+    padding-left: 8px;
+  }
+}
+
 .base-expandable-content {
   overflow: hidden; /* 隐藏溢出的内容 */
   transition: max-height 0.3s ease; /* 平滑过渡效果 */
 }
+
 /* 横向布局容器 */
 .horizontal-list {
   display: flex;
@@ -360,28 +394,29 @@ const handleDelete = async (id: number) => {
 
 /* 拖拽项样式 */
 .horizontal-item {
-  flex: 0 0 auto;
+  display: flex;
   min-width: 150px;
   padding: 20px;
+  cursor: move;
   background: #d3a137;
   border: 2px solid #e0e0e0;
   border-radius: 8px;
-  cursor: move;
-  user-select: none; /* 防止文字选中:ml-citation{ref="7" data="citationList"} */
   transition:
     transform 0.3s,
     box-shadow 0.3s;
-  display: flex;
+  user-select: none; /* 防止文字选中:ml-citation{ref="7" data="citationList"} */
+  flex: 0 0 auto;
   align-items: center;
   gap: 10px;
 }
 
 /* 拖拽手柄样式 */
 .drag-handle {
-  opacity: 0.5;
   cursor: move;
+  opacity: 0.5;
   transition: opacity 0.3s;
 }
+
 .drag-handle:hover {
   opacity: 1;
 }
@@ -389,31 +424,31 @@ const handleDelete = async (id: number) => {
 /* 拖拽时的悬停效果 */
 .horizontal-item:hover {
   transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
 }
 
 /* 滚动条样式 */
 .horizontal-list::-webkit-scrollbar {
   height: 8px;
 }
+
 .horizontal-list::-webkit-scrollbar-thumb {
   background: #888;
   border-radius: 4px;
 }
 
 .sortable-container {
-  cursor: move;
   display: flex;
+  padding: 7px;
+  overflow-y: auto;
+  cursor: move;
   flex-direction: column;
   gap: 9px;
-  //max-height: 80vh;
-  overflow-y: auto;
-  padding: 7px;
 }
 
 .sortable-item {
   display: flex;
-  align-items: center;
+  height: 50%;
   padding: 8px;
   background: #fff;
   border: 1px solid #ebeef5;
@@ -422,24 +457,24 @@ const handleDelete = async (id: number) => {
     transform 0.3s,
     box-shadow 0.3s;
   user-select: none;
-  height: 50%;
+  align-items: center;
 }
 
 .sortable-item:hover {
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
 }
 
 .order-number {
+  display: flex;
   width: 24px;
   height: 24px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #409eff;
-  color: white;
-  border-radius: 50%;
   margin-right: 7px;
   font-weight: bold;
+  color: white;
+  background: #409eff;
+  border-radius: 50%;
+  align-items: center;
+  justify-content: center;
 }
 
 .drag-handle {
@@ -461,8 +496,244 @@ const handleDelete = async (id: number) => {
 .sortable-container::-webkit-scrollbar {
   width: 8px;
 }
+
 .sortable-container::-webkit-scrollbar-thumb {
   background: #c0c4cc;
   border-radius: 4px;
 }
+
+.route-form-page {
+  display: flex;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  flex-direction: column;
+  gap: 12px;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 66px;
+  padding: 12px 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.page-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.page-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.page-header h1 {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.page-header p {
+  margin: 1px 0 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.route-form {
+  display: flex;
+  flex: 1;
+  min-height: 0;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.form-section {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-lighter);
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-heading > span,
+.section-heading__title > span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.section-heading--actions {
+  justify-content: space-between;
+}
+
+.section-heading__title {
+  display: flex;
+  align-items: center;
+}
+
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  column-gap: 24px;
+  padding: 18px 18px 2px;
+}
+
+.form-grid__wide {
+  grid-column: 1 / -1;
+}
+
+.route-form :deep(.el-form-item) {
+  margin-bottom: 16px;
+}
+
+.route-form :deep(.el-input),
+.route-form :deep(.el-select),
+.route-form :deep(.el-tree-select) {
+  width: 100%;
+}
+
+.inspection-section {
+  display: flex;
+  min-height: 280px;
+  flex: 1;
+  flex-direction: column;
+}
+
+.inspection-section :deep(.el-empty) {
+  flex: 1;
+}
+
+.sortable-container {
+  display: flex;
+  min-height: 0;
+  padding: 14px;
+  overflow-y: auto;
+  cursor: default;
+  flex: 1;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.sortable-item {
+  display: flex;
+  height: auto;
+  min-height: 58px;
+  padding: 10px 12px;
+  cursor: grab;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+  align-items: center;
+  transition:
+    border-color 0.2s ease,
+    box-shadow 0.2s ease;
+  user-select: none;
+}
+
+.sortable-item:hover {
+  border-color: var(--el-color-primary-light-7);
+  box-shadow: 0 3px 10px rgb(0 0 0 / 6%);
+}
+
+.sortable-item:active {
+  cursor: grabbing;
+}
+
+.order-number {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 26px;
+  height: 26px;
+  margin-right: 2px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #fff;
+  background: var(--el-color-primary);
+  border-radius: 50%;
+  flex: 0 0 auto;
+}
+
+.drag-handle {
+  display: flex;
+  padding: 8px 12px;
+  font-size: 18px;
+  color: var(--el-text-color-placeholder);
+  cursor: grab;
+  opacity: 1;
+}
+
+.component-content {
+  display: grid;
+  grid-template-columns: minmax(180px, 1fr) minmax(280px, 2fr);
+  min-width: 0;
+  flex: 1;
+  gap: 16px;
+}
+
+.item-field {
+  display: flex;
+  min-width: 0;
+  align-items: flex-start;
+}
+
+.item-field__label {
+  margin-right: 8px;
+  font-weight: 600;
+  color: var(--el-text-color-regular);
+  white-space: nowrap;
+}
+
+.item-field__value {
+  min-width: 0;
+  color: var(--el-text-color-secondary);
+  overflow-wrap: anywhere;
+}
+
+.action-bar {
+  position: sticky;
+  bottom: 0;
+  z-index: 5;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  min-height: 56px;
+  padding: 10px 16px;
+  margin-top: auto;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 -4px 12px rgb(0 0 0 / 5%);
+}
 </style>

+ 267 - 144
src/views/pms/inspect/route/index.vue

@@ -1,146 +1,147 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm px-2 mb-2"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('route.RouteName')" prop="routeName" style="margin-left: 25px">
-            <el-input
-              v-model="queryParams.routeName"
-              :placeholder="t('route.nHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <!--      <el-form-item :label="t('monitor.category')" prop="deviceClassify">-->
-          <!--        <el-input-->
-          <!--          v-model="queryParams.deviceClassify"-->
-          <!--          :placeholder="t('deviceForm.categoryHolder')"-->
-          <!--          clearable-->
-          <!--          @keyup.enter="handleQuery"-->
-          <!--          class="!w-240px"-->
-          <!--        />-->
-          <!--      </el-form-item>-->
-          <el-form-item :label="t('monitor.category')" prop="deviceClassify" style="width: 15vw">
-            <el-tree-select
-              v-model="queryParams.deviceClassify"
-              :data="productClassifyList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              :placeholder="t('deviceForm.categoryHolder')"
-              filterable
-            />
-          </el-form-item>
-          <el-form-item :label="t('monitor.deviceName')" prop="deviceName">
-            <el-input
-              v-model="queryParams.deviceName"
-              :placeholder="t('iotDevice.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('file.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('file.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-inspect-route:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('fault.added') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-inspect-route:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="inspect-route-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="inspect-route-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="88px"
+      class="inspect-route-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('route.RouteName')" prop="routeName">
+          <el-input
+            v-model="queryParams.routeName"
+            :placeholder="t('route.nHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('monitor.category')" prop="deviceClassify">
+          <el-tree-select
+            v-model="queryParams.deviceClassify"
+            :data="productClassifyList"
+            :props="defaultProps"
+            check-strictly
+            node-key="id"
+            :placeholder="t('deviceForm.categoryHolder')"
+            filterable
+            clearable
+            class="query-control" />
+        </el-form-item>
+        <el-form-item :label="t('monitor.deviceName')" prop="deviceName">
+          <el-input
+            v-model="queryParams.deviceName"
+            :placeholder="t('iotDevice.nameHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+      </div>
+
+      <el-form-item class="query-actions">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('file.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('file.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['rq:iot-inspect-route:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('fault.added') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-inspect-route:export']">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div
+      class="table-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+              settings-cache-key="pms-inspect-route-list">
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn :label="t('route.RouteName')" prop="routeName" />
+              <ZmTableColumn :label="t('monitor.category')" prop="deviceClassifyName" />
+              <ZmTableColumn :label="t('monitor.deviceName')" prop="deviceName" />
+              <ZmTableColumn :label="t('iotDevice.brand')" prop="brandName" />
+              <ZmTableColumn
+                :label="t('common.createTime')"
+                prop="createTime"
+                :formatter="dateFormatter" />
+              <ZmTableColumn
+                :label="t('operationFill.operation')"
+                :width="120"
+                action
+                fixed="right">
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-route:update']">
+                    {{ t('fault.edit') }}
+                  </el-button>
+                  <el-button
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                    v-hasPermi="['rq:iot-inspect-route:delete']">
+                    {{ t('fault.del') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 10px">
-        <zm-table :loading="loading" :data="list" height="64vh">
-          <zm-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <el-table-column :label="t('route.RouteName')" align="center" prop="routeName" />
-          <zm-table-column
-            :label="t('monitor.category')"
-            align="center"
-            prop="deviceClassifyName"
-          />
-          <zm-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName" />
-          <!--      <el-table-column label="巡检项" align="center" prop="itemJson" />-->
-          <zm-table-column :label="t('iotDevice.brand')" align="center" prop="brandName" />
-          <zm-table-column
-            :label="t('common.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180px"
-          />
-          <!--      <el-table-column label="部门id" align="center" prop="deptId" />-->
-          <zm-table-column
-            :label="t('operationFill.operation')"
-            align="center"
-            min-width="120px"
-            action
-          >
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-route:update']"
-              >
-                {{ t('fault.edit') }}
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-inspect-route:delete']"
-              >
-                {{ t('fault.del') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
-        <Pagination
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
 
   <!-- 表单弹窗:添加/修改 -->
   <IotInspectRouteForm ref="formRef" @success="getList" />
@@ -151,18 +152,19 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotInspectRouteApi, IotInspectRouteVO } from '@/api/pms/inspect/route'
 import IotInspectRouteForm from './IotInspectRouteForm.vue'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { defaultProps } from '@/utils/tree'
+import { useUserStore } from '@/store/modules/user'
 const { push } = useRouter() // 路由跳转
 /** 巡检路线 列表 */
 defineOptions({ name: 'IotInspectRoute' })
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotInspectRouteVO>()
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-let isLeftContentCollapsed = ref(false)
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 const loading = ref(true) // 列表的加载中
 const list = ref<IotInspectRouteVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
@@ -183,8 +185,9 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-const handleDeptNodeClick = async (row) => {
+const handleDeptNodeClick = async (row: Tree) => {
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 /** 查询列表 */
@@ -207,10 +210,20 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
+const handleSizeChange = (pageSize: number) => {
+  queryParams.pageSize = pageSize
+  handleQuery()
+}
+
+const handleCurrentChange = (pageNo: number) => {
+  queryParams.pageNo = pageNo
+  getList()
+}
+
 /** 添加/修改操作 */
 const openForm = (type: string, id?: number) => {
   //修改
@@ -255,3 +268,113 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<style scoped>
+.inspect-route-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  min-width: 0;
+  gap: 12px 24px;
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+@media (width >= 1800px) {
+  .inspect-route-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .inspect-route-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 175px;
+  }
+}
+
+@media (width <= 1200px) {
+  .inspect-route-page {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  :deep(.inspect-route-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .inspect-route-query,
+  .table-panel {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions,
+  .query-control {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+  }
+}
+</style>

+ 308 - 426
src/views/pms/iotmainworkorder/DeviceAlarmBomList.vue

@@ -1,137 +1,166 @@
 <template>
-  <Dialog v-model="dialogVisible"
-          :title="t('monitor.details')"
-          :width="dialogWidth"
-          class="fixed-height-dialog"
-          @close="handleClose" >
-    <ContentWrap>
-
-      <!-- 添加设备信息展示区域 -->
-      <div v-if="deviceInfo" class="device-info-card">
-        <div class="info-item">
-          <span class="info-label">{{ t('iotDevice.code') }}:</span>
-          <span class="info-value">{{ deviceInfo.deviceCode }}</span>
-        </div>
-        <div class="info-item">
-          <span class="info-label">{{ t('iotDevice.name') }}:</span>
-          <span class="info-value">{{ deviceInfo.deviceName }}</span>
-        </div>
-        <div class="info-item" v-if="deviceInfo.model">
-          <span class="info-label">{{ t('deviceForm.model') }}:</span>
-          <span class="info-value">{{ deviceInfo.model }}</span>
+  <Dialog
+    v-model="dialogVisible"
+    :title="t('monitor.details')"
+    :width="dialogWidth"
+    class="device-alarm-detail-dialog"
+    @close="handleClose">
+    <template #title>
+      <div class="dialog-title">
+        <div class="dialog-title__icon"><Icon icon="ep:calendar" /></div>
+        <div>
+          <div class="dialog-title__text">{{ t('monitor.details') }}</div>
+          <div class="dialog-title__subtitle">查看设备保养项目及各维度保养里程</div>
         </div>
       </div>
-
-      <div class="table-container">
-        <!-- 添加表格容器并设置滚动 -->
-        <el-table
-          v-loading="loading"
-          :data="paginatedList"
-          :stripe="true"
-          :show-overflow-tooltip="true"
-          style="width: auto"
-          height="100%"
-          class="scrollable-table"
-          ref="tableRef"
-        >
-          <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode"
-                           :width="columnWidths.deviceCode" v-if="false"/>
-          <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName"
-                           :width="columnWidths.deviceName" v-if="false"/>
-          <el-table-column :label="t('mainPlan.MaintItems')" align="center" prop="name"
-                           :width="columnWidths.name"/>
-
-          <el-table-column :label="t('operationFillForm.sumTime')" align="center" prop="totalRunTime"
-                           :width="columnWidths.totalRunTime">
-            <template #default="{ row }">
-              {{ row.totalRunTime ?? row.tempTotalRunTime }}
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('operationFillForm.sumKil')" align="center" prop="totalMileage"
-                           :width="columnWidths.totalMileage">
-            <template #default="{ row }">
-              {{ row.totalMileage ?? row.tempTotalMileage }}
-            </template>
-          </el-table-column>
-
-          <!-- 时间分组列 -->
-          <el-table-column v-if="showTimeColumns" label="保养时长"
-                           align="center"
-                           :width="columnWidths.timeGroup">
-            <el-table-column :label="t('mainPlan.lastMaintenanceOperationTime')" align="center" prop="lastRunningTime"
-                             :width="columnWidths.lastRunningTime"/>
-            <el-table-column :label="t('mainPlan.RunTimeCycle')" align="center" prop="nextRunningTime"
-                             :width="columnWidths.nextRunningTime"/>
-            <el-table-column :label="t('mainPlan.nextMaintTime')" align="center" prop="timePeriod"
-                             :width="columnWidths.timePeriod">
+    </template>
+
+    <div class="dialog-content">
+      <section v-if="deviceInfo" class="device-info-section">
+        <div class="section-heading"><span></span>设备信息</div>
+        <div class="device-info-grid">
+          <div class="info-item">
+            <span class="info-label">{{ t('iotDevice.code') }}</span>
+            <span class="info-value">{{ deviceInfo.deviceCode }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-label">{{ t('iotDevice.name') }}</span>
+            <span class="info-value">{{ deviceInfo.deviceName }}</span>
+          </div>
+          <div class="info-item" v-if="deviceInfo.model">
+            <span class="info-label">{{ t('deviceForm.model') }}</span>
+            <span class="info-value">{{ deviceInfo.model }}</span>
+          </div>
+        </div>
+      </section>
+
+      <section class="maintenance-section">
+        <div class="section-heading"><span></span>保养项目</div>
+        <div class="table-container">
+          <ZmTable
+            :loading="loading"
+            :data="paginatedList"
+            :show-border="true"
+            settings-cache-key="pms-device-maintenance-alarm-detail">
+            <ZmTableColumn
+              :label="t('iotDevice.code')"
+              align="center"
+              prop="deviceCode"
+              v-if="false" />
+            <ZmTableColumn
+              :label="t('iotDevice.name')"
+              align="center"
+              prop="deviceName"
+              v-if="false" />
+            <ZmTableColumn :label="t('mainPlan.MaintItems')" align="center" prop="name" />
+
+            <ZmTableColumn
+              :label="t('operationFillForm.sumTime')"
+              align="center"
+              prop="totalRunTime">
               <template #default="{ row }">
-                <span :class="{ 'negative-value': isNegative(row.timePeriod) }">
-                  {{ row.timePeriod }}
-                </span>
+                {{ row.totalRunTime ?? row.tempTotalRunTime }}
               </template>
-            </el-table-column>
-          </el-table-column>
-
-          <!-- 里程分组列 -->
-          <el-table-column v-if="showMileageColumns" label="保养里程"
-                           align="center"
-                           :width="columnWidths.mileageGroup">
-            <el-table-column :label="t('mainPlan.lastMaintenanceMileage')" align="center" prop="lastRunningKilometers"
-                             :width="columnWidths.lastRunningKilometers"/>
-            <el-table-column :label="t('mainPlan.operatingMileageCycle')" align="center" prop="nextRunningKilometers"
-                             :width="columnWidths.nextRunningKilometers"/>
-            <el-table-column :label="t('mainPlan.nextMaintKil')" align="center" prop="kilometerCycle"
-                             :width="columnWidths.kilometerCycle">
+            </ZmTableColumn>
+            <ZmTableColumn
+              :label="t('operationFillForm.sumKil')"
+              align="center"
+              prop="totalMileage">
               <template #default="{ row }">
-                <span :class="{ 'negative-value': isNegative(row.kilometerCycle) }">
-                  {{ row.kilometerCycle }}
-                </span>
+                {{ row.totalMileage ?? row.tempTotalMileage }}
               </template>
-            </el-table-column>
-          </el-table-column>
-
-          <!-- 日期分组列 -->
-          <el-table-column v-if="showNaturalDateColumns" label="保养日期"
-                           align="center"
-                           :width="columnWidths.dateGroup">
-            <el-table-column :label="t('mainPlan.lastMaintenanceNaturalDate')" align="center" prop="lastNaturalDate"
-                             :width="columnWidths.lastNaturalDate">
-              <template #default="scope">
-                <el-date-picker
-                  v-model="scope.row.lastNaturalDate"
-                  type="date"
-                  placeholder="选择日期"
-                  format="YYYY-MM-DD"
-                  value-format="YYYY-MM-DD"
-                  style="width: 100%"
-                  :disabled="true"
-                />
-              </template>
-            </el-table-column>
-            <el-table-column :label="t('mainPlan.NaturalDailyCycle')" align="center" prop="nextNaturalDate"
-                             :width="columnWidths.nextNaturalDate"/>
-            <el-table-column :label="t('mainPlan.nextMaintDate')" align="center" prop="naturalDatePeriod"
-                             :width="columnWidths.naturalDatePeriod"/>
-          </el-table-column>
-        </el-table>
-      </div>
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="handlePagination"
-      />
-    </ContentWrap>
+            </ZmTableColumn>
+
+            <!-- 时间分组列 -->
+            <ZmTableColumn v-if="showTimeColumns" label="保养时长" align="center">
+              <ZmTableColumn
+                :label="t('mainPlan.lastMaintenanceOperationTime')"
+                align="center"
+                prop="lastRunningTime" />
+              <ZmTableColumn
+                :label="t('mainPlan.RunTimeCycle')"
+                align="center"
+                prop="nextRunningTime" />
+              <ZmTableColumn :label="t('mainPlan.nextMaintTime')" align="center" prop="timePeriod">
+                <template #default="{ row }">
+                  <span :class="{ 'negative-value': isNegative(row.timePeriod) }">
+                    {{ row.timePeriod }}
+                  </span>
+                </template>
+              </ZmTableColumn>
+            </ZmTableColumn>
+
+            <!-- 里程分组列 -->
+            <ZmTableColumn v-if="showMileageColumns" label="保养里程" align="center">
+              <ZmTableColumn
+                :label="t('mainPlan.lastMaintenanceMileage')"
+                align="center"
+                prop="lastRunningKilometers" />
+              <ZmTableColumn
+                :label="t('mainPlan.operatingMileageCycle')"
+                align="center"
+                prop="nextRunningKilometers" />
+              <ZmTableColumn
+                :label="t('mainPlan.nextMaintKil')"
+                align="center"
+                prop="kilometerCycle">
+                <template #default="{ row }">
+                  <span :class="{ 'negative-value': isNegative(row.kilometerCycle) }">
+                    {{ row.kilometerCycle }}
+                  </span>
+                </template>
+              </ZmTableColumn>
+            </ZmTableColumn>
+
+            <!-- 日期分组列 -->
+            <ZmTableColumn v-if="showNaturalDateColumns" label="保养日期" align="center">
+              <ZmTableColumn
+                :label="t('mainPlan.lastMaintenanceNaturalDate')"
+                align="center"
+                prop="lastNaturalDate"
+                min-width="160">
+                <template #default="scope">
+                  <el-date-picker
+                    v-model="scope.row.lastNaturalDate"
+                    type="date"
+                    placeholder="选择日期"
+                    format="YYYY-MM-DD"
+                    value-format="YYYY-MM-DD"
+                    style="width: 100%"
+                    :disabled="true" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('mainPlan.NaturalDailyCycle')"
+                align="center"
+                prop="nextNaturalDate" />
+              <ZmTableColumn
+                :label="t('mainPlan.nextMaintDate')"
+                align="center"
+                prop="naturalDatePeriod" />
+            </ZmTableColumn>
+          </ZmTable>
+        </div>
+        <Pagination
+          class="mr-4"
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="handlePagination" />
+      </section>
+    </div>
   </Dialog>
 </template>
 
 <script setup lang="ts">
 import { DictDataVO } from '@/api/system/dict/dict.data'
 import dayjs from 'dayjs'
-import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
+import { IotMainWorkOrderBomApi } from '@/api/pms/iotmainworkorderbom'
 import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
-import {propTypes} from "@/utils/propTypes";
+import { propTypes } from '@/utils/propTypes'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { t } = useI18n() // 国际化
+const { ZmTable, ZmTableColumn } = useTableComponents<IotMaintenanceBomVO>()
 const emit = defineEmits(['close']) // 定义 success 事件,用于操作成功后的回调
 const dialogVisible = ref(false) // 弹窗的是否展示
 const loading = ref(true) // 列表的加载中
@@ -145,10 +174,7 @@ const shouldResetPagination = ref(false)
 // 添加外部传入的设备信息
 const externalDeviceInfo = ref(null)
 
-const dialogWidth = '1500px';
-
-const tableRef = ref(null)    // 表格实例引用
-const columnWidths = ref({}) // 存储列宽的对象
+const dialogWidth = 'min(1200px, 92vw)'
 
 const queryParams = reactive({
   pageNo: 1,
@@ -159,69 +185,11 @@ const queryParams = reactive({
 })
 
 const props = defineProps({
-  flag: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
+  flag: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired
 })
 
 const selectedRow = ref(null)
 
-const getTextWidth = (str: string) => {
-  if (!str) return 0;
-  if (widthCache.has(str)) return widthCache.get(str)!;
-
-  const span = document.createElement('span');
-  span.style.visibility = 'hidden';
-  span.style.position = 'absolute';
-  span.style.whiteSpace = 'nowrap';
-  span.style.font = '14px Microsoft YaHei';
-  span.textContent = str;
-
-  document.body.appendChild(span);
-  const width = span.offsetWidth;
-  document.body.removeChild(span);
-
-  widthCache.set(str, width);
-  return width;
-};
-
-// 添加列宽计算函数
-const widthCache = new Map<string, number>();
-
-const flexColumnWidth = (label: string, prop: keyof IotMaintenanceBomVO) => {
-  // 确保有数据时才计算
-  if (!paginatedList.value.length) return "auto";
-  // 基础内边距30px
-  const basePadding = 20;
-  const labelWidth = getActualWidth(label) + basePadding; // 文本宽度 + 内边距
-  // 计算内容最大宽度
-  let maxContentWidth = 0;
-  // 获取该列所有内容的宽度
-  for (const row of paginatedList.value) {
-    let value = "";
-
-    // 特殊列处理
-    if (prop === "totalRunTime") {
-      value = (row.totalRunTime ?? row.tempTotalRunTime)?.toString() || "";
-    } else if (prop === "totalMileage") {
-      value = (row.totalMileage ?? row.tempTotalMileage)?.toString() || "";
-    } else {
-      value = row[prop]?.toString() || "";
-    }
-
-    // 数值格式化
-    if (value && !isNaN(Number(value))) {
-      value = Number(value).toLocaleString();
-    }
-
-    const contentWidth = getActualWidth(value) + basePadding;
-    if (contentWidth > maxContentWidth) {
-      maxContentWidth = contentWidth;
-    }
-  }
-
-  // 返回较大值加上安全边距
-  return Math.max(labelWidth, maxContentWidth, 120) + "px";
-};
-
 // 处理单选逻辑
 const selectRow = (row) => {
   selectedRow.value = selectedRow.value?.id === row.id ? null : row
@@ -231,82 +199,21 @@ const selectRow = (row) => {
 
 // 分页事件处理
 const handlePagination = () => {
-  console.log("分页变化,当前页:", queryParams.pageNo);
-};
-
-// 改进的宽度计算函数
-const getActualWidth = (text: string) => {
-  if (!text) return 0;
-
-  // 更精准的字符宽度计算
-  const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
-  const otherChars = text.length - chineseChars;
-
-  // 中文16px,英文9px
-  return chineseChars * 16 + otherChars * 9;
-};
-
-// 设置列宽
-const setColumnWidths = () => {
-  const widths: Record<string, string> = {};
-
-  // 固定列
-  // widths.deviceCode = flexColumnWidth(t('iotDevice.code'), 'deviceCode');
-  // widths.deviceName = flexColumnWidth(t('iotDevice.name'), 'deviceName');
-  widths.totalRunTime = flexColumnWidth(t('operationFillForm.sumTime'), 'totalRunTime');
-  widths.totalMileage = flexColumnWidth(t('operationFillForm.sumKil'), 'totalMileage');
-  widths.name = flexColumnWidth(t('mainPlan.MaintItems'), 'name');
-
-  // 动态列
-  if (showTimeColumns.value) {
-    widths.lastRunningTime = flexColumnWidth(t('mainPlan.lastMaintenanceOperationTime'), 'lastRunningTime');
-    widths.nextRunningTime = flexColumnWidth(t('mainPlan.RunTimeCycle'), 'nextRunningTime');
-    widths.timePeriod = flexColumnWidth(t('mainPlan.nextMaintTime'), 'timePeriod');
-    // 分组列宽度 = 子列宽度之和 + 边框补偿
-    widths.timeGroup = `${[
-      parseFloat(widths.lastRunningTime),
-      parseFloat(widths.nextRunningTime),
-      parseFloat(widths.timePeriod)
-    ].reduce((sum, w) => sum + w, 0) + 2}px`;
-  }
-
-  if (showMileageColumns.value) {
-    widths.lastRunningKilometers = flexColumnWidth(t('mainPlan.lastMaintenanceMileage'), 'lastRunningKilometers');
-    widths.nextRunningKilometers = flexColumnWidth(t('mainPlan.operatingMileageCycle'), 'nextRunningKilometers');
-    widths.kilometerCycle = flexColumnWidth(t('mainPlan.nextMaintKil'), 'kilometerCycle');
-    widths.mileageGroup = `${[
-      parseFloat(widths.lastRunningKilometers),
-      parseFloat(widths.nextRunningKilometers),
-      parseFloat(widths.kilometerCycle)
-    ].reduce((sum, w) => sum + w, 0) + 2}px`;
-  }
-
-  if (showNaturalDateColumns.value) {
-    widths.lastNaturalDate = flexColumnWidth(t('mainPlan.lastMaintenanceOperationTime'), 'lastNaturalDate'); // 固定日期选择器的宽度
-    widths.nextNaturalDate = flexColumnWidth(t('mainPlan.NaturalDailyCycle'), 'nextNaturalDate');
-    widths.naturalDatePeriod = flexColumnWidth(t('mainPlan.nextMaintDate'), 'naturalDatePeriod');
-    widths.dateGroup = `${[
-      parseFloat(widths.lastNaturalDate),
-      parseFloat(widths.nextNaturalDate),
-      parseFloat(widths.naturalDatePeriod)
-    ].reduce((sum, w) => sum + w, 0) + 2}px`;
-  }
-
-  columnWidths.value = widths;
-};
+  console.log('分页变化,当前页:', queryParams.pageNo)
+}
 
 // 分页计算属性
 const paginatedList = computed(() => {
-  const start = (queryParams.pageNo - 1) * queryParams.pageSize;
-  const end = start + queryParams.pageSize;
-  return list.value.slice(start, end);
-});
+  const start = (queryParams.pageNo - 1) * queryParams.pageSize
+  const end = start + queryParams.pageSize
+  return list.value.slice(start, end)
+})
 
 const open = async (id?: number, flag?: string, deviceInfo?: any) => {
   // 重置分页参数
   queryParams.pageNo = 1
   queryParams.pageSize = 10
-  list.value = []  // 清空列表避免显示旧数据
+  list.value = [] // 清空列表避免显示旧数据
   total.value = 0
 
   await nextTick() // 确保DOM更新完成
@@ -314,7 +221,7 @@ const open = async (id?: number, flag?: string, deviceInfo?: any) => {
     externalDeviceInfo.value = deviceInfo
     queryParams.deviceId = deviceInfo.deviceId // 如果需要的话
   }
-  if('workOrder' === flag) {
+  if ('workOrder' === flag) {
     // 加载保养工单 BOM
     queryParams.workOrderId = id
     queryParams.planId = undefined
@@ -334,7 +241,7 @@ const getWorkOrderList = async () => {
   try {
     const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams)
     // 格式化日期字段
-    data.forEach(item => {
+    data.forEach((item) => {
       if (item.lastNaturalDate) {
         // 将时间戳转换为 YYYY-MM-DD 格式
         item.lastNaturalDate = dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
@@ -343,9 +250,9 @@ const getWorkOrderList = async () => {
         item.lastNaturalDate = ''
       }
       // 计算下次保养运行时长 H
-      item.timePeriod = calculateTimePeriod(item);
+      item.timePeriod = calculateTimePeriod(item)
       // 计算下次保养公里数 KM
-      item.kilometerCycle = calculateKiloPeriod(item);
+      item.kilometerCycle = calculateKiloPeriod(item)
       // 计算下次保养日期
       item.naturalDatePeriod = calculateNextNaturalDate(item)
     })
@@ -357,12 +264,11 @@ const getWorkOrderList = async () => {
 }
 
 const getPlanList = async () => {
-  widthCache.clear();
   loading.value = true
   try {
     const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams)
     // 格式化日期字段
-    data.forEach(item => {
+    data.forEach((item) => {
       if (item.lastNaturalDate) {
         // 将时间戳转换为 YYYY-MM-DD 格式
         item.lastNaturalDate = dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
@@ -371,9 +277,9 @@ const getPlanList = async () => {
         item.lastNaturalDate = ''
       }
       // 计算下次保养运行时长 H
-      item.timePeriod = calculateTimePeriod(item);
+      item.timePeriod = calculateTimePeriod(item)
       // 计算下次保养公里数 KM
-      item.kilometerCycle = calculateKiloPeriod(item);
+      item.kilometerCycle = calculateKiloPeriod(item)
       // 计算下次保养日期
       item.naturalDatePeriod = calculateNextNaturalDate(item)
     })
@@ -381,7 +287,6 @@ const getPlanList = async () => {
     total.value = data.length
   } finally {
     loading.value = false
-    nextTick(setColumnWidths); // 数据加载后计算列宽
   }
 }
 
@@ -389,18 +294,18 @@ const getPlanList = async () => {
 const deviceInfo = computed(() => {
   // 优先使用外部传入的设备信息
   if (externalDeviceInfo.value) {
-    return externalDeviceInfo.value;
+    return externalDeviceInfo.value
   }
   if (list.value.length > 0) {
-    const firstRecord = list.value[0];
+    const firstRecord = list.value[0]
     return {
       deviceCode: firstRecord.deviceCode,
       deviceName: firstRecord.deviceName,
       model: firstRecord.model // 确保列表数据中也有 model
-    };
+    }
   }
-  return null;
-});
+  return null
+})
 
 const handleClose = () => {
   // 重置状态避免多个弹窗出现
@@ -419,32 +324,30 @@ const handleClose = () => {
 // 计算 距离下次保养运行时长 H
 const calculateTimePeriod = (item: IotMaintenanceBomVO) => {
   if (item.runningTimeRule === 0) {
-    const totalRunVal = item.totalRunTime ?? item.tempTotalRunTime;
-    const next = Number(item.nextRunningTime) || 0;
-    const totalRun = totalRunVal != null ? Number(totalRunVal) : 0;
-    const lastRun = Number(item.lastRunningTime) || 0;
-    const result = next - (totalRun - lastRun);
-    return Number(result.toFixed(2));
+    const totalRunVal = item.totalRunTime ?? item.tempTotalRunTime
+    const next = Number(item.nextRunningTime) || 0
+    const totalRun = totalRunVal != null ? Number(totalRunVal) : 0
+    const lastRun = Number(item.lastRunningTime) || 0
+    const result = next - (totalRun - lastRun)
+    return Number(result.toFixed(2))
   }
-  return typeof item.timePeriod === 'number'
-    ? Number(item.timePeriod.toFixed(2))
-    : item.timePeriod;
-};
+  return typeof item.timePeriod === 'number' ? Number(item.timePeriod.toFixed(2)) : item.timePeriod
+}
 
 // 计算 距离下次保养公里数 KM
 const calculateKiloPeriod = (item: IotMaintenanceBomVO) => {
   if (item.mileageRule === 0) {
-    const totalRunVal = item.totalMileage ?? item.tempTotalMileage;
-    const next = Number(item.nextRunningKilometers) || 0;
-    const totalRun = totalRunVal != null ? Number(totalRunVal) : 0;
-    const lastRun = Number(item.lastRunningKilometers) || 0;
-    const result = next - (totalRun - lastRun);
-    return Number(result.toFixed(2));
+    const totalRunVal = item.totalMileage ?? item.tempTotalMileage
+    const next = Number(item.nextRunningKilometers) || 0
+    const totalRun = totalRunVal != null ? Number(totalRunVal) : 0
+    const lastRun = Number(item.lastRunningKilometers) || 0
+    const result = next - (totalRun - lastRun)
+    return Number(result.toFixed(2))
   }
   return typeof item.kilometerCycle === 'number'
     ? Number(item.kilometerCycle.toFixed(2))
-    : item.kilometerCycle;
-};
+    : item.kilometerCycle
+}
 
 // 计算下次保养日期
 const calculateNextNaturalDate = (item: IotMaintenanceBomVO): string => {
@@ -456,18 +359,18 @@ const calculateNextNaturalDate = (item: IotMaintenanceBomVO): string => {
 
 // 计算属性:控制时间相关列的显示
 const showTimeColumns = computed(() => {
-  return paginatedList.value.some(item => item.runningTimeRule === 0);
-});
+  return paginatedList.value.some((item) => item.runningTimeRule === 0)
+})
 
 // 计算属性:控制里程相关列的显示
 const showMileageColumns = computed(() => {
-  return paginatedList.value.some(item => item.mileageRule === 0);
-});
+  return paginatedList.value.some((item) => item.mileageRule === 0)
+})
 
 // 计算属性:自然日期相关列的显示
 const showNaturalDateColumns = computed(() => {
-  return paginatedList.value.some(item => item.naturalDateRule === 0);
-});
+  return paginatedList.value.some((item) => item.naturalDateRule === 0)
+})
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
@@ -486,192 +389,171 @@ const resetQuery = () => {
 
 // 判断是否为负数的辅助函数
 const isNegative = (value: any): boolean => {
-  if (value === null || value === undefined || value === '') return false;
-  const num = Number(value);
-  return !isNaN(num) && num < 0;
-};
-
-// 监听分页数据变化,重新设置列宽
-watch(paginatedList, () => {
-  nextTick(() => {
-    setColumnWidths();
-  });
-});
-
-// 监听动态列的变化
-watch([showTimeColumns, showMileageColumns, showNaturalDateColumns], () => {
-  nextTick(setColumnWidths);
-});
-
+  if (value === null || value === undefined || value === '') return false
+  const num = Number(value)
+  return !isNaN(num) && num < 0
+}
 </script>
 
 <style lang="scss" scoped>
+@media (width <= 768px) {
+  .dialog-title__subtitle {
+    display: none;
+  }
+
+  .device-info-grid {
+    grid-template-columns: minmax(0, 1fr);
+    padding: 12px;
+  }
+
+  .maintenance-section .table-container {
+    padding-right: 12px;
+    padding-left: 12px;
+  }
+}
 
-/* 设备信息卡片样式 */
 .device-info-card {
   display: flex;
-  flex-wrap: wrap;
-  gap: 24px; /* 项间距 */
-  margin-bottom: 16px;
   padding: 16px;
+  margin-bottom: 16px;
   background-color: #f8f9fa;
   border-radius: 8px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
+  flex-wrap: wrap;
+  gap: 24px; /* 项间距 */
 
   .info-item {
     display: flex;
     align-items: center;
 
     .info-label {
+      margin-right: 8px;
       font-weight: 600;
       color: #606266;
-      margin-right: 8px;
       white-space: nowrap;
     }
 
     .info-value {
+      min-width: 200px;
+      padding: 4px 12px;
       font-weight: 500;
       color: #303133;
-      padding: 4px 12px;
-      background: #ffffff;
-      border-radius: 4px;
+      background: #fff;
       border: 1px solid #ebeef5;
-      min-width: 200px;
+      border-radius: 4px;
     }
   }
 }
 
-/* 分组表头样式 */
-:deep(.el-table .el-table__header .el-table-column--group) {
-  background-color: #f5f7fa;
-  font-weight: bold;
-
-  > .cell {
-    font-weight: bold;
-    color: #303133;
-  }
-
-  background-color: #f0f7ff;
-  border: 1px solid #409eff !important;
-  box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
-
-  > .cell {
-    font-weight: 700;
-    color: #1d63dc;
-  }
+/* 负数值样式 */
+.negative-value {
+  font-weight: 600;
+  color: #f56c6c;
 }
 
-/* 加深所有单元格边框 */
-:deep(.el-table__header th) {
-  border: 1px solid #c0c4cc !important;
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
 }
 
-/* 分组内部单元格特殊样式 */
-:deep(.el-table-column--group .el-table__cell) {
-  border-top: 1px dashed #a0cfff !important;
+.dialog-title__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  font-size: 18px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
 }
 
-/* 分组间垂直线 */
-:deep(.el-table-column--group) {
-  position: relative;
-
-  &::after {
-    content: "";
-    position: absolute;
-    right: -1px;
-    top: 0;
-    bottom: 0;
-    width: 2px;
-    background: linear-gradient(to bottom, #409eff, #79bbff);
-  }
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
 }
 
-/* 新版CSS解决方案 */
-.fixed-height-dialog {
-  :deep(.el-dialog) {
-    width: 1500px !important; /* 固定宽度 */
-    height: 85vh !important; /* 使用视口高度 */
-    display: flex;
-    flex-direction: column;
-    margin-top: 5vh !important; /* 垂直居中 */
-  }
-
-  /* 添加媒体查询,确保在小屏幕上也能完整显示 */
-  @media (max-width: 1500px) {
-    :deep(.el-dialog) {
-      width: 95% !important;
-      max-width: 1500px; /* 仍然限制最大宽度 */
-    }
-  }
+.dialog-title__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
 
-  :deep(.el-dialog__header) {
-    padding: 20px;
-    flex-shrink: 0;
-  }
+.dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
 
-  :deep(.el-dialog__body) {
-    flex: 1;
-    padding: 10px 20px;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden; /* 防止内容溢出 */
-  }
+.device-info-section,
+.maintenance-section {
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
 }
 
-/* 表格容器 */
-.table-container {
-  flex: 1;
-  overflow: auto;
-  position: relative;
-
-  .scrollable-table {
-    width: 100%; /* 自适应宽度 */
-
-    :deep(.el-table__header) {
-      th {
-        white-space: nowrap !important; /* 强制表头不换行 */
-        text-overflow: ellipsis;
-        overflow: hidden;
-        padding: 8px 0; /* 增加内边距 */
-      }
-    }
+.section-heading {
+  display: flex;
+  align-items: center;
+  height: 44px;
+  padding: 0 16px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
 
-    :deep(.el-table__body) {
-      td {
-        white-space: nowrap !important; /* 强制内容不换行 */
-        text-overflow: ellipsis;
-        overflow: hidden;
-        padding: 8px 0; /* 增加内边距 */
-      }
-    }
+.section-heading span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
 
-    :deep(.el-table__body-wrapper) {
-      overflow-x: hidden !important; /* 隐藏X轴滚动条 */
-    }
-  }
+.device-info-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 12px;
+  padding: 14px 16px;
 }
 
-/* 分页样式 */
-.pagination-footer {
-  margin-top: 15px;
-  flex-shrink: 0;
+.device-info-grid .info-item {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  padding: 10px 12px;
+  background: var(--el-fill-color-extra-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
 }
 
-/* 响应式处理 - 确保在小屏幕上布局合理 */
-@media (max-width: 1500px) {
-  .fixed-height-dialog :deep(.el-dialog) {
-    width: 95% !important;
-    max-width: 98vw;
-  }
+.device-info-grid .info-label {
+  flex: 0 0 auto;
+  margin-right: 10px;
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
 
-  .table-container {
-    overflow-x: auto;
-  }
+.device-info-grid .info-value {
+  min-width: 0;
+  overflow: hidden;
+  font-weight: 500;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-/* 负数值样式 */
-.negative-value {
-  color: #f56c6c;
-  font-weight: 600;
+.maintenance-section .table-container {
+  padding: 14px 16px 0;
 }
+
+/* 设备信息卡片样式 */
 </style>

+ 17 - 25
src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue

@@ -208,8 +208,7 @@ onMounted(() => {
 
 <template>
   <div
-    class="device-main-alarm-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
-  >
+    class="device-main-alarm-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
     <DeptTreeSelect
       :top-id="rootDeptId"
       :deptId="deptId"
@@ -218,16 +217,14 @@ onMounted(() => {
       :show-title="false"
       request-api="getSimpleDeptList"
       class="device-main-alarm-tree row-span-2"
-      @node-click="handleDeptNodeClick"
-    />
+      @node-click="handleDeptNodeClick" />
 
     <el-form
       ref="queryFormRef"
       :model="queryParams"
       size="default"
-      label-width="68px"
-      class="device-main-alarm-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0"
-    >
+      label-width="80px"
+      class="device-main-alarm-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
       <div class="query-row">
         <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
           <el-input
@@ -235,8 +232,7 @@ onMounted(() => {
             :placeholder="t('iotDevice.codeHolder')"
             clearable
             class="query-control"
-            @keyup.enter="handleQuery"
-          />
+            @keyup.enter="handleQuery" />
         </el-form-item>
         <el-form-item :label="t('iotDevice.name')" prop="deviceName">
           <el-input
@@ -244,8 +240,7 @@ onMounted(() => {
             :placeholder="t('iotDevice.nameHolder')"
             clearable
             class="query-control"
-            @keyup.enter="handleQuery"
-          />
+            @keyup.enter="handleQuery" />
         </el-form-item>
       </div>
 
@@ -261,8 +256,7 @@ onMounted(() => {
           plain
           :loading="exportLoading"
           @click="handleExport"
-          v-hasPermi="['rq:iot-device:export']"
-        >
+          v-hasPermi="['rq:iot-device:export']">
           <Icon icon="ep:download" class="mr-5px" />导出
         </el-button>
       </el-form-item>
@@ -278,15 +272,13 @@ onMounted(() => {
               :width="width"
               :height="height"
               :max-height="height"
-              show-border
-            >
+              show-border>
               <ZmTableColumn
                 type="index"
                 :label="t('monitor.serial')"
                 :width="70"
                 fixed="left"
-                hide-in-column-settings
-              />
+                hide-in-column-settings />
               <ZmTableColumn prop="deviceCode" :label="t('iotDevice.code')" fixed="left" />
               <ZmTableColumn prop="deviceName" :label="t('iotDevice.name')" fixed="left">
                 <template #default="{ row }">
@@ -309,14 +301,12 @@ onMounted(() => {
                 v-if="showRunTime"
                 prop="totalRunTime"
                 label="累计运行时长H"
-                min-width="140"
-              />
+                min-width="140" />
               <ZmTableColumn
                 v-if="showMileage"
                 prop="totalMileage"
                 label="累计运行里程KM"
-                min-width="150"
-              />
+                min-width="150" />
               <ZmTableColumn v-if="showMultiAttrs" label="多属性累计值" min-width="200">
                 <template #default="{ row }">
                   <template v-for="(value, key) in row.multiAttrsTotalRuntime" :key="key">
@@ -352,8 +342,7 @@ onMounted(() => {
                     link
                     type="primary"
                     @click="openBomForm(row)"
-                    v-hasPermi="['rq:iot-device:query']"
-                  >
+                    v-hasPermi="['rq:iot-device:query']">
                     {{ t('monitor.details') }}
                   </el-button>
                 </template>
@@ -374,8 +363,7 @@ onMounted(() => {
           :total="total"
           layout="total, sizes, prev, pager, next, jumper"
           @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-        />
+          @current-change="handleCurrentChange" />
       </div>
     </div>
   </div>
@@ -401,6 +389,10 @@ onMounted(() => {
   min-width: 0;
 }
 
+.query-row :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
 .query-actions {
   flex: 0 0 auto;
 }

+ 250 - 115
src/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue

@@ -1,156 +1,291 @@
 <template>
   <el-drawer
-    :title="t('bomList.materialDetail')"
     :append-to-body="true"
     :model-value="modelValue"
-    @update:model-value="$emit('update:modelValue', $event)"
     :show-close="false"
     direction="rtl"
     :size="computedSize"
-    :before-close="handleClose"
-  >
-    <template v-if="nodeId">
-      <div v-loading="loading" style="height: 100%">
-        <el-table :data="filteredMaterials" style="width: 100%">
-          <el-table-column prop="bomNodeId" :label="t('bomList.bomNode')" width="180" v-if="false"/>
-          <el-table-column prop="factory" :label="t('workOrderMaterial.factory')" width="180" v-if="!hideExtraColumns"/>
-          <el-table-column prop="costCenter" :label="t('workOrderMaterial.costCenter')" width="180" v-if="!hideExtraColumns"/>
-          <el-table-column prop="projectDepartment" :label="t('workOrderMaterial.storageLocation')"  width="180" v-if="!hideExtraColumns"/>
-          <el-table-column prop="materialName" :label="t('workOrderMaterial.materialName')" width="180" />
-          <el-table-column prop="materialCode" :label="t('workOrderMaterial.materialCode')" width="180" />
-          <el-table-column prop="unit" :label="t('workOrderMaterial.unit')" width="180" />
-          <el-table-column prop="unitPrice" :label="t('workOrderMaterial.unitPrice')" width="180" />
-          <el-table-column prop="quantity" :label="t('workOrderMaterial.ConsumptionQuantity')" width="180" />
-          <el-table-column prop="materialSource" :label="t('bomList.type')" width="180" />
-          <el-table-column :label="t('workplace.operation')" align="right" v-if="!props.detail">
-            <template #default="scope">
-              <el-button
-                size="small"
-                type="danger"
-                @click="handleDelete(scope.row)"
-              >{{ t('form.delete') }}</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="loadMaterials(props.nodeId)"
-        />
-        -->
+    class="selected-material-drawer"
+    @update:model-value="emit('update:modelValue', $event)">
+    <template #header>
+      <div class="drawer-header">
+        <div class="drawer-header__main">
+          <div class="drawer-header__icon">
+            <Icon icon="ep:goods" :size="22" />
+          </div>
+          <div>
+            <div class="drawer-title">{{ t('bomList.materialDetail') }}</div>
+            <div class="drawer-subtitle">查看当前维修项目关联的物料明细</div>
+          </div>
+        </div>
+        <el-button circle @click="handleClose">
+          <Icon icon="ep:close" />
+        </el-button>
       </div>
     </template>
 
+    <div class="drawer-body">
+      <section class="material-section">
+        <div class="section-header">
+          <div class="section-title">
+            <span class="section-marker"></span>
+            <span>关联物料</span>
+          </div>
+          <span class="material-total">共 {{ filteredMaterials.length }} 条</span>
+        </div>
+
+        <div class="table-container">
+          <ZmTable
+            :data="filteredMaterials"
+            :loading="loading"
+            :show-border="true"
+            settings-cache-key="pms-maintain-selected-material">
+            <ZmTableColumn
+              v-if="!hideExtraColumns"
+              prop="factory"
+              :label="t('workOrderMaterial.factory')"
+              min-width="140" />
+            <ZmTableColumn
+              v-if="!hideExtraColumns"
+              prop="costCenter"
+              :label="t('workOrderMaterial.costCenter')"
+              min-width="150" />
+            <ZmTableColumn
+              v-if="!hideExtraColumns"
+              prop="projectDepartment"
+              :label="t('workOrderMaterial.storageLocation')"
+              min-width="150" />
+            <ZmTableColumn
+              prop="materialName"
+              :label="t('workOrderMaterial.materialName')"
+              min-width="180" />
+            <ZmTableColumn
+              prop="materialCode"
+              :label="t('workOrderMaterial.materialCode')"
+              min-width="150" />
+            <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" width="100" />
+            <ZmTableColumn
+              prop="unitPrice"
+              :label="t('workOrderMaterial.unitPrice')"
+              min-width="120" />
+            <ZmTableColumn
+              prop="quantity"
+              :label="t('workOrderMaterial.ConsumptionQuantity')"
+              min-width="130" />
+            <ZmTableColumn prop="materialSource" :label="t('bomList.type')" min-width="120" />
+            <ZmTableColumn
+              v-if="!detail"
+              :label="t('workplace.operation')"
+              width="100"
+              fixed="right"
+              action>
+              <template #default="{ row }">
+                <el-button link type="danger" @click="handleDelete(row)">
+                  <Icon icon="ep:delete" class="mr-4px" />{{ t('form.delete') }}
+                </el-button>
+              </template>
+            </ZmTableColumn>
+          </ZmTable>
+        </div>
+      </section>
+    </div>
+
+    <template #footer>
+      <div class="drawer-footer">
+        <el-button @click="handleClose">关闭</el-button>
+      </div>
+    </template>
   </el-drawer>
 </template>
+
 <script setup lang="ts">
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 
-import { ref, watch, defineOptions, defineEmits } from 'vue'
-import { ElMessage } from 'element-plus'
-import * as PmsMaterialApi from '@/api/pms/material'
-const drawerVisible = ref<boolean>(false)
-const emit = defineEmits(['update:modelValue', 'add', 'delete'])
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
+interface MaterialRow {
+  id?: number
+  bomNodeId?: number | string
+  factory?: string
+  costCenter?: string
+  projectDepartment?: string
+  materialName?: string
+  materialCode?: string
+  unit?: string
+  unitPrice?: number
+  quantity?: number
+  materialSource?: string
+}
 
-defineOptions({
-  name: 'SelectedMaterialDrawer'
-})
+defineOptions({ name: 'SelectedMaterialDrawer' })
 
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  createTime: [],
-  bomId: '',
-  name: '',
-  code: ''
-})
+const props = withDefaults(
+  defineProps<{
+    modelValue: boolean
+    nodeId?: number | string
+    detail?: boolean
+    materials?: MaterialRow[]
+    hideExtraColumns?: boolean
+  }>(),
+  {
+    detail: false,
+    materials: () => [],
+    hideExtraColumns: false
+  }
+)
 
-const windowWidth = ref(window.innerWidth)
-// 动态计算百分比
-const computedSize = computed(() => {
-  return windowWidth.value > 1200 ? '60%' : '80%'
-})
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'add', row: MaterialRow): void
+  (e: 'delete', row: MaterialRow): void
+}>()
 
+const { t } = useI18n()
+const message = useMessage()
+const { ZmTable, ZmTableColumn } = useTableComponents<MaterialRow>()
 const loading = ref(false)
-const total = ref(0) // 列表的总页数
-// const materials = ref([])
-
-const props = defineProps({
-  modelValue: Boolean,
-  nodeId: Number,
-  detail: Boolean,
-  materials: Array, // 接收父组件传递的完整物料列表
-  hideExtraColumns: {
-    type: Boolean,
-    default: false
-  }
-})
+const windowWidth = ref(window.innerWidth)
 
-// 监听bom树节点ID变化
-watch(() => props.nodeId, (newVal) => {
-  console.log('传递过来的nodeId', newVal)
-  if (newVal) {
-    // await loadMaterials(newVal)
-  }
+const computedSize = computed(() => {
+  if (windowWidth.value <= 768) return '100%'
+  return windowWidth.value > 1440 ? '68%' : '82%'
 })
 
-// 计算属性过滤当前节点物料
 const filteredMaterials = computed(() => {
-  return props.materials?.filter(item =>
-    item.bomNodeId === props.nodeId
-  ) || []
+  if (!props.nodeId) return []
+  return props.materials.filter((item) => item.bomNodeId === props.nodeId)
 })
 
-// 加载指定bom节点下的物料数据
-const loadMaterials = async (nodeId) => {
-  queryParams.bomId = nodeId
-  try {
-    loading.value = true
-    // API调用
-    const data = await PmsMaterialApi.listByBomId(queryParams)
-    materials.value = data.list
-    total.value = data.total
-  } catch (error) {
-    ElMessage.error('数据加载失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 删除物料前的确认 */
-const handleDelete = async (row: any) => {
+const handleDelete = async (row: MaterialRow) => {
   try {
     await message.delConfirm()
-    // 用户确认后触发删除事件
     emit('delete', row)
-    // message.success(t('common.delSuccess'))
-  } catch (error) {
-    // 用户取消操作
-    console.log('删除操作已取消')
-  }
+  } catch {}
+}
+
+const handleClose = () => {
+  emit('update:modelValue', false)
 }
 
-// 打开抽屉
 const openDrawer = () => {
-  drawerVisible.value = true
+  emit('update:modelValue', true)
 }
 
-// 关闭抽屉
 const closeDrawer = () => {
-  drawerVisible.value = false
+  handleClose()
 }
 
-// 关闭抽屉
-const handleClose = () => {
-  emit('update:modelValue', false)
-  // materials.value = []
+const updateWindowWidth = () => {
+  windowWidth.value = window.innerWidth
 }
 
-defineExpose({ openDrawer, closeDrawer }) // 暴露方法给父组件
+onMounted(() => window.addEventListener('resize', updateWindowWidth))
+onUnmounted(() => window.removeEventListener('resize', updateWindowWidth))
 
+defineExpose({ openDrawer, closeDrawer })
 </script>
 
-<style lang="scss" scoped></style>
+<style scoped>
+.drawer-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  width: 100%;
+}
+
+.drawer-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.drawer-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 44px;
+  height: 44px;
+  margin-right: 12px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.drawer-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.drawer-subtitle {
+  margin-top: 4px;
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+}
+
+.drawer-body {
+  height: 100%;
+  padding: 4px;
+}
+
+.material-section {
+  height: 100%;
+  overflow: hidden;
+  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;
+  min-height: 54px;
+  padding: 0 18px;
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.section-marker {
+  width: 4px;
+  height: 18px;
+  margin-right: 10px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.material-total {
+  padding: 4px 10px;
+  font-size: 12px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border-radius: 4px;
+}
+
+.table-container {
+  padding: 16px;
+}
+
+.drawer-footer {
+  display: flex;
+  justify-content: flex-end;
+}
+
+@media (width <= 768px) {
+  .drawer-subtitle {
+    display: none;
+  }
+
+  .table-container {
+    padding: 12px;
+  }
+}
+</style>

+ 515 - 257
src/views/pms/iotprojectinfo/IotProjectInfoForm.vue

@@ -1,247 +1,268 @@
 <template>
-  <ContentWrap v-loading="formLoading">
+  <div class="project-form-page">
+    <ContentWrap class="page-header">
+      <div class="page-header__main">
+        <div class="page-header__icon">
+          <Icon :icon="formType === 'create' ? 'ep:document-add' : 'ep:edit-pen'" />
+        </div>
+        <div>
+          <h2 class="page-header__title">
+            {{ formType === 'create' ? '新增项目信息' : '编辑项目信息' }}
+          </h2>
+          <p class="page-header__subtitle">维护客户、合同、归属部门及项目配置信息</p>
+        </div>
+      </div>
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </ContentWrap>
+
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="120px"
-      v-loading="formLoading"
-    >
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="客户名称" prop="manufacturerId">
-            <el-select
-              clearable
-              @clear="zzClear"
-              v-model="formData.manufactureName"
-              :placeholder="t('deviceForm.mfgHolder')"
-              @click="openCustomerZz"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('project.payment')" prop="payment">
-            <el-select v-model="formData.payment" placeholder="请选择" clearable>
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_PROJECT_SETTLEMENT)"
-                :key="dict.id"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="合同名称" prop="contractName">
-            <el-input v-model="formData.contractName" placeholder="请输入合同名称" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="合同编号" prop="contractCode">
-            <el-input v-model="formData.contractCode" placeholder="请输入合同编号" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="开始时间" prop="startTime">
-            <el-date-picker
-              style="width: 150%"
-              v-model="formData.startTime"
-              type="date"
-              value-format="x"
-              placeholder="选择开始时间"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="完成时间" prop="endTime">
-            <el-date-picker
-              style="width: 150%"
-              v-model="formData.endTime"
-              type="date"
-              value-format="x"
-              placeholder="选择完成时间"
-            />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="12">
-          <el-form-item :label="t('iotDevice.company')" prop="deptId">
-            <el-tree-select
-              :disabled="isDeptDisabled"
-              v-model="formData.deptId"
-              :data="deptList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              filterable
-              placeholder="请选择所属公司"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="项目部" prop="projectDeptId">
-            <el-select
-              v-model="formData.projectDeptId"
-              :options="deptOptions"
-              filterable
-              placeholder="请选择项目部"
-              clearable
-            />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="责任人" prop="responsiblePerson">
+      label-width="108px"
+      size="default"
+      class="project-form">
+      <ContentWrap class="project-form-card">
+        <div class="form-card-header">
+          <div class="form-card-title">
+            <span></span>
+            <h3>合同信息</h3>
+          </div>
+          <span class="form-card-hint">客户、合同及项目周期信息</span>
+        </div>
+        <div class="form-card-body">
+          <el-row :gutter="24">
+            <el-col :xs="24" :md="12">
+              <el-form-item label="客户名称" prop="manufacturerId">
+                <el-select
+                  v-model="formData.manufactureName"
+                  clearable
+                  :placeholder="t('deviceForm.mfgHolder')"
+                  @clear="zzClear"
+                  @click="openCustomerZz" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item :label="t('project.payment')" prop="payment">
+                <el-select v-model="formData.payment" placeholder="请选择" clearable>
+                  <el-option
+                    v-for="dict in getStrDictOptions(DICT_TYPE.PMS_PROJECT_SETTLEMENT)"
+                    :key="dict.id"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="合同名称" prop="contractName">
+                <el-input v-model="formData.contractName" placeholder="请输入合同名称" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="合同编号" prop="contractCode">
+                <el-input v-model="formData.contractCode" placeholder="请输入合同编号" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="开始时间" prop="startTime">
+                <el-date-picker
+                  v-model="formData.startTime"
+                  type="date"
+                  value-format="x"
+                  placeholder="选择开始时间" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="完成时间" prop="endTime">
+                <el-date-picker
+                  v-model="formData.endTime"
+                  type="date"
+                  value-format="x"
+                  placeholder="选择完成时间" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </ContentWrap>
+
+      <ContentWrap class="project-form-card">
+        <div class="form-card-header">
+          <div class="form-card-title">
+            <span></span>
+            <h3>项目配置</h3>
+          </div>
+          <span class="form-card-hint">归属部门、责任人与项目属性</span>
+        </div>
+        <div class="form-card-body">
+          <el-row :gutter="24">
+            <el-col :xs="24" :md="12">
+              <el-form-item :label="t('iotDevice.company')" prop="deptId">
+                <el-tree-select
+                  v-model="formData.deptId"
+                  :disabled="isDeptDisabled"
+                  :data="deptList"
+                  :props="defaultProps"
+                  check-strictly
+                  node-key="id"
+                  filterable
+                  placeholder="请选择所属公司" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="项目部" prop="projectDeptId">
+                <el-select
+                  v-model="formData.projectDeptId"
+                  :options="deptOptions"
+                  :loading="deptLoading"
+                  filterable
+                  placeholder="请选择项目部"
+                  clearable />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="责任人" prop="responsiblePerson">
+                <div class="responsible-person-control">
+                  <el-tooltip
+                    v-if="selectedUserNames"
+                    effect="dark"
+                    placement="top"
+                    :content="getFullSelectedUserNames()">
+                    <span class="user-names">{{ selectedUserNames }}</span>
+                  </el-tooltip>
+                  <span v-else class="user-names user-names--empty">暂未选择责任人</span>
+                  <el-button
+                    type="primary"
+                    plain
+                    size="default"
+                    :disabled="!formData.deptId"
+                    @click="openUserDialog">
+                    <Icon icon="ep:user" class="mr-5px" />选择
+                  </el-button>
+                </div>
+                <div v-if="!formData.deptId" class="el-form-item__error">请先选择所属公司</div>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item :label="t('project.overseaFlag')" prop="overseas">
+                <el-select v-model="formData.overseas" placeholder="请选择" clearable>
+                  <el-option
+                    v-for="dict in getIntDictOptions(DICT_TYPE.PMS_PROJECT_OVERSEA_FLAG)"
+                    :key="dict.id"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item :label="t('project.overseaArea')" prop="overseasArea">
+                <el-select v-model="formData.overseasArea" placeholder="请选择" clearable>
+                  <el-option
+                    v-for="dict in getStrDictOptions(DICT_TYPE.PMS_PROJECT_AREA)"
+                    :key="dict.id"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item :label="t('project.workArea')" prop="location">
+                <el-autocomplete
+                  ref="workAreaAutocomplete"
+                  v-model="formData.location"
+                  :fetch-suggestions="querySearch"
+                  :trigger-on-focus="true"
+                  :disabled="!workAreaOptions.length"
+                  placeholder="请输入施工区域"
+                  popper-class="work-area-autocomplete"
+                  @focus="handleWorkAreaFocus"
+                  @select="handleSelect">
+                  <template #prefix>
+                    <el-icon v-if="loadingWorkAreaOptions" class="is-loading">
+                      <Loading />
+                    </el-icon>
+                  </template>
+                </el-autocomplete>
+                <div v-if="!workAreaOptions.length" class="el-form-item__error">
+                  暂无可用施工区域选项,请手动输入
+                </div>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :md="12">
+              <el-form-item label="虚拟项目" prop="contractSubject">
+                <el-switch
+                  v-model="formData.contractSubject"
+                  active-value="Y"
+                  inactive-value="N"
+                  active-text="是"
+                  inactive-text="否" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item label="备注" prop="remark">
+                <el-input
+                  v-model="formData.remark"
+                  placeholder="请输入备注"
+                  type="textarea"
+                  :rows="3" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </ContentWrap>
+    </el-form>
+
+    <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
+
+    <el-dialog
+      v-model="userDialogVisible"
+      width="920px"
+      :before-close="handleUserDialogClose"
+      class="user-select-dialog">
+      <template #header>
+        <div class="dialog-title">
+          <div class="dialog-title__icon"><Icon icon="ep:user" /></div>
+          <div>
+            <div class="dialog-title__text">选择责任人</div>
+            <div class="dialog-title__subtitle">从当前公司人员中选择项目责任人</div>
+          </div>
+        </div>
+      </template>
+      <div class="transfer-container">
+        <el-transfer
+          v-model="selectedUserIds"
+          :data="userList"
+          :titles="['可选人员', '已选人员']"
+          :props="{ key: 'id', label: 'nickname' }"
+          filterable
+          class="transfer-component">
+          <template #default="{ option }">
             <el-tooltip
-              v-if="selectedUserNames"
               effect="dark"
               placement="top"
-              :content="getFullSelectedUserNames()"
-            >
-              <span class="user-names">
-                {{ selectedUserNames }}
+              :content="`${option.nickname} - ${option.deptName || '未分配部门'}`">
+              <span class="transfer-option-text">
+                {{ option.nickname }} - {{ option.deptName || '未分配部门' }}
               </span>
             </el-tooltip>
-            <el-button
-              type="primary"
-              size="small"
-              @click="openUserDialog"
-              style="margin-left: 10px"
-              :disabled="!formData.deptId"
-            >
-              选择责任人
-            </el-button>
-            <div v-if="!formData.deptId" class="el-form-item__error">请先选择所属公司</div>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('project.overseaFlag')" prop="overseas">
-            <el-select v-model="formData.overseas" placeholder="请选择" clearable>
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.PMS_PROJECT_OVERSEA_FLAG)"
-                :key="dict.id"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="12">
-          <el-form-item :label="t('project.overseaArea')" prop="overseasArea">
-            <el-select v-model="formData.overseasArea" placeholder="请选择" clearable>
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_PROJECT_AREA)"
-                :key="dict.id"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item :label="t('project.workArea')" prop="location">
-            <el-autocomplete
-              ref="workAreaAutocomplete"
-              v-model="formData.location"
-              :fetch-suggestions="querySearch"
-              :trigger-on-focus="true"
-              placeholder="请输入施工区域"
-              @focus="handleWorkAreaFocus"
-              @select="handleSelect"
-              :disabled="!workAreaOptions.length"
-              popper-class="work-area-autocomplete"
-            >
-              <template #prefix>
-                <el-icon v-if="loadingWorkAreaOptions" class="is-loading">
-                  <Loading />
-                </el-icon>
-              </template>
-            </el-autocomplete>
-            <div v-if="!workAreaOptions.length" class="el-form-item__error">
-              暂无可用施工区域选项,请手动输入
-            </div>
-          </el-form-item>
-        </el-col>
-      </el-row>
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="虚拟项目" prop="contractSubject">
-            <el-switch
-              v-model="formData.contractSubject"
-              active-value="Y"
-              inactive-value="N"
-              active-text="是"
-              inactive-text="否"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="备注" prop="remark">
-            <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-    </el-form>
-  </ContentWrap>
-
-  <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
-
-  <!-- 责任人选择对话框 -->
-  <el-dialog
-    v-model="userDialogVisible"
-    title="选择责任人"
-    width="1000px"
-    :before-close="handleUserDialogClose"
-    class="user-select-dialog"
-  >
-    <div class="transfer-container">
-      <el-transfer
-        v-model="selectedUserIds"
-        :data="userList"
-        :titles="['可选人员', '已选人员']"
-        :props="{ key: 'id', label: 'nickname' }"
-        filterable
-        class="transfer-component"
-      >
-        <template #default="{ option }">
-          <el-tooltip
-            effect="dark"
-            placement="top"
-            :content="`${option.nickname} - ${option.deptName || '未分配部门'}`"
-          >
-            <span class="transfer-option-text">
-              {{ option.nickname }} - {{ option.deptName || '未分配部门' }}
-            </span>
-          </el-tooltip>
-        </template>
-      </el-transfer>
+          </template>
+        </el-transfer>
+      </div>
+      <template #footer>
+        <el-button size="default" @click="handleUserDialogClose">取消</el-button>
+        <el-button size="default" type="primary" @click="confirmUserSelection">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <div class="form-actions">
+      <el-button size="default" type="primary" :loading="formLoading" @click="submitForm">
+        <Icon icon="ep:check" class="mr-5px" />保存
+      </el-button>
+      <el-button size="default" @click="close">取消</el-button>
     </div>
-    <template #footer>
-      <span class="dialog-footer">
-        <el-button @click="handleUserDialogClose">取消</el-button>
-        <el-button type="primary" @click="confirmUserSelection">确定</el-button>
-      </span>
-    </template>
-  </el-dialog>
-
-  <ContentWrap>
-    <el-form style="float: right">
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="close">取 消</el-button>
-    </el-form>
-  </ContentWrap>
+  </div>
 </template>
 <script setup lang="ts">
 import { IotProjectInfoApi, IotProjectInfoVO } from '@/api/pms/iotprojectinfo'
@@ -916,59 +937,296 @@ const handleSelect = (item: any) => {
 </script>
 
 <style scoped>
+.project-form-page {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+}
+
+.page-header {
+  margin-bottom: 0;
+}
+
+.page-header :deep(.el-card__body) {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 64px;
+  padding: 12px 16px;
+}
+
+.page-header__main,
+.dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.page-header__icon,
+.dialog-title__icon {
+  display: inline-flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 38px;
+  height: 38px;
+  margin-right: 12px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 6px;
+}
+
+.page-header__title {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 24px;
+  letter-spacing: 0;
+  color: var(--el-text-color-primary);
+}
+
+.page-header__subtitle,
+.dialog-title__subtitle {
+  margin: 2px 0 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.project-form {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  gap: 14px;
+}
+
+.project-form-card {
+  margin-bottom: 0;
+  overflow: hidden;
+  border-radius: 8px;
+}
+
+.project-form-card :deep(.el-card__body) {
+  padding: 0;
+}
+
+.form-card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 42px;
+  padding: 0 16px;
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.form-card-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.form-card-title > span {
+  width: 4px;
+  height: 17px;
+  margin-right: 9px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.form-card-header h3 {
+  margin: 0;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.form-card-hint {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
+
+.form-card-body {
+  padding: 18px 18px 2px;
+}
+
+.project-form :deep(.el-select),
+.project-form :deep(.el-tree-select),
+.project-form :deep(.el-date-editor),
+.project-form :deep(.el-autocomplete) {
+  width: 100%;
+}
+
+.project-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.project-form :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+.responsible-person-control {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+  min-width: 0;
+}
+
 .user-names {
-  display: inline-block;
-  max-height: 32px;
-  min-width: 200px;
-  padding: 0 10px;
-  background-color: #f5f7fa;
-  border: 1px solid #dcdfe6;
+  display: block;
+  height: 32px;
+  min-width: 0;
+  padding: 0 11px;
+  overflow: hidden;
+  line-height: 30px;
+  color: var(--el-text-color-regular);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  border: 1px solid var(--el-border-color);
   border-radius: 4px;
+  flex: 1;
+}
+
+.user-names--empty {
+  color: var(--el-text-color-placeholder);
+}
+
+.form-actions {
+  position: sticky;
+  bottom: 0;
+  z-index: 10;
+  display: flex;
+  flex-direction: row-reverse;
+  gap: 10px;
+  align-items: center;
+  min-height: 58px;
+  padding: 10px 16px;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 -4px 12px rgb(0 0 0 / 4%);
+}
+
+.form-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.dialog-title__text {
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.user-select-dialog :deep(.el-dialog__header) {
+  padding: 16px 20px;
+  margin-right: 0;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.user-select-dialog :deep(.el-dialog__body) {
+  padding: 18px 20px;
+}
+
+.user-select-dialog :deep(.el-dialog__footer) {
+  padding: 12px 20px;
+  border-top: 1px solid var(--el-border-color-lighter);
 }
 
 .transfer-container {
-  padding: 0;
-  text-align: center;
+  width: 100%;
 }
 
-/* 3. 直接调整 el-transfer 左右窗口的宽度 */
 :deep(.el-transfer-panel) {
-  width: 40% !important; /* 强制设置左右面板宽度,确保两侧面板对等 */
+  flex: 1;
+  width: auto;
 }
 
 .transfer-component {
+  display: flex;
+  align-items: center;
   width: 100%;
-  min-width: 600px;
+  min-width: 0;
+}
+
+.transfer-component :deep(.el-transfer__buttons) {
+  flex: 0 0 auto;
+  padding: 0 16px;
 }
 
 :deep(.el-transfer-panel__item) {
-  display: flex !important;
-  height: 32px !important;
-  padding: 0 8px !important;
-  margin: 0 !important;
+  display: flex;
+  align-items: center;
+  height: 32px;
+  padding: 0 8px;
+  margin: 0;
   overflow: hidden;
-  line-height: 32px !important;
+  line-height: 32px;
   text-overflow: ellipsis;
   white-space: nowrap;
-  align-items: center !important;
 }
 
 .transfer-option-text {
   display: inline-block;
   max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 :deep(.el-transfer-panel__list) {
-  width: 100% !important; /* 使面板内部内容宽度占满 */
+  width: 100%;
+}
+
+:global(.work-area-autocomplete .el-autocomplete-suggestion__list) {
+  max-height: 300px;
+  overflow-y: auto;
 }
 
-/* 添加全局样式,用于自定义下拉框 */
-.work-area-autocomplete .el-autocomplete-suggestion__list {
-  max-height: 300px; /* 设置最大高度 */
-  overflow-y: auto; /* 添加垂直滚动条 */
+:global(.work-area-autocomplete .el-autocomplete-suggestion__wrap) {
+  max-height: 300px;
 }
 
-.work-area-autocomplete .el-autocomplete-suggestion__wrap {
-  max-height: 300px; /* 设置最大高度 */
+@media (width <= 768px) {
+  .page-header :deep(.el-card__body) {
+    align-items: flex-start;
+  }
+
+  .page-header__subtitle {
+    display: none;
+  }
+
+  .form-card-hint {
+    display: none;
+  }
+
+  .form-card-body {
+    padding-right: 12px;
+    padding-left: 12px;
+  }
+
+  .user-select-dialog :deep(.el-dialog) {
+    width: calc(100% - 24px) !important;
+  }
+
+  .transfer-component {
+    display: block;
+  }
+
+  .transfer-component :deep(.el-transfer-panel) {
+    width: 100%;
+  }
+
+  .transfer-component :deep(.el-transfer__buttons) {
+    display: flex;
+    justify-content: center;
+    padding: 12px 0;
+  }
 }
 </style>

+ 38 - 54
src/views/pms/iotprojectinfo/index.vue

@@ -64,23 +64,23 @@
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
             class="query-control query-control--date" />
         </el-form-item>
-        <el-form-item class="query-actions">
-          <el-button type="primary" @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', undefined)"
-            v-hasPermi="['rq:iot-project-info:create']">
-            <Icon icon="ep:plus" class="mr-5px" />新增
-          </el-button>
-          <el-button type="success" plain :loading="exportLoading" @click="handleExport">
-            <Icon icon="ep:download" class="mr-5px" />导出
-          </el-button>
-        </el-form-item>
       </div>
+      <el-form-item class="query-actions">
+        <el-button type="primary" @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', undefined)"
+          v-hasPermi="['rq:iot-project-info:create']">
+          <Icon icon="ep:plus" class="mr-5px" />新增
+        </el-button>
+        <el-button type="success" plain :loading="exportLoading" @click="handleExport">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
     </el-form>
 
     <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
@@ -819,11 +819,14 @@ const openForm = (type: string, id?: number) => {
 const assignTask = (row: IotProjectInfoVO) => {
   push({
     name: 'IotProjectTaskInfo',
-    query: {
+    params: {
+      type: 'create',
       projectId: row.id,
-      contractName: row.contractName
+      deptId: row.deptId
     },
-    params: { type: 'create' }
+    query: {
+      contractName: row.contractName
+    }
   })
 }
 
@@ -896,17 +899,9 @@ watch(
 </script>
 
 <style scoped>
-@media (width <= 1500px) {
+@media (width <= 1600px) {
   .iot-project-info-query .query-row {
-    gap: 12px 18px;
-  }
-
-  .iot-project-info-query .query-control {
-    width: 210px;
-  }
-
-  .iot-project-info-query .query-control--date {
-    width: 280px;
+    grid-template-columns: repeat(3, minmax(240px, 1fr));
   }
 }
 
@@ -919,12 +914,8 @@ watch(
     );
   }
 
-  .iot-project-info-query .query-actions {
-    width: 100%;
-  }
-
-  .iot-project-info-query .query-actions :deep(.el-form-item__content) {
-    justify-content: flex-start;
+  .iot-project-info-query .query-row {
+    grid-template-columns: repeat(2, minmax(240px, 1fr));
   }
 }
 
@@ -933,15 +924,8 @@ watch(
     padding: 12px;
   }
 
-  .iot-project-info-query .query-row,
-  .iot-project-info-query .query-row :deep(.el-form-item),
-  .iot-project-info-query .query-actions {
-    width: 100%;
-  }
-
-  .iot-project-info-query .query-control,
-  .iot-project-info-query .query-control--date {
-    width: 100%;
+  .iot-project-info-query .query-row {
+    grid-template-columns: minmax(0, 1fr);
   }
 
   .iot-project-info-query .query-actions :deep(.el-form-item__content) {
@@ -958,26 +942,26 @@ watch(
 }
 
 .iot-project-info-query {
-  display: block;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
 }
 
 .query-row {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 12px 24px;
+  display: grid;
+  grid-template-columns: repeat(4, minmax(220px, 1fr)) minmax(320px, 1.4fr);
+  gap: 12px 20px;
   min-width: 0;
 }
 
 .query-actions {
-  margin-left: auto;
-  flex: 0 0 auto;
+  width: 100%;
 }
 
 .query-actions :deep(.el-form-item__content) {
   display: flex;
   flex-wrap: wrap;
-  justify-content: flex-end;
+  justify-content: flex-start;
   gap: 8px 10px;
 }
 
@@ -986,11 +970,11 @@ watch(
 }
 
 .query-control {
-  width: 210px;
+  width: 100%;
 }
 
 .query-control--date {
-  width: 300px;
+  width: 100%;
 }
 
 :deep(.el-form-item) {

Разлика између датотеке није приказан због своје велике величине
+ 776 - 707
src/views/pms/iotprojecttask/IotProjectTaskForm.vue


+ 249 - 68
src/views/pms/iotprojecttask/index.vue

@@ -531,21 +531,45 @@ onMounted(async () => {
 
   <el-dialog
     v-model="planDialogVisible"
-    :title="`${currentRow?.contractName || ''} - ${currentRow?.wellName || ''} - 任务计划`"
-    width="80%">
-    <div class="mb-15px">
-      <el-button type="primary" @click="addNewRow">
-        <Icon icon="ep:plus" class="mr-5px" />新增行
-      </el-button>
-    </div>
+    width="min(1440px, calc(100vw - 40px))"
+    class="plan-dialog">
+    <template #header>
+      <div class="plan-dialog-title">
+        <span class="plan-dialog-title__icon">
+          <Icon icon="ep:calendar" />
+        </span>
+        <div class="plan-dialog-title__content">
+          <h3>任务计划</h3>
+          <p>
+            {{ currentRow?.contractName || '未关联合同' }}
+            <span class="plan-dialog-title__separator">·</span>
+            {{ currentRow?.wellName || '未填写井号' }}
+          </p>
+        </div>
+      </div>
+    </template>
 
-    <el-table :data="planList" border stripe>
-      <el-table-column label="序号" width="60" align="center">
-        <template #default="scope">
-          {{ scope.$index + 1 }}
-        </template>
-      </el-table-column>
-      <!-- <el-table-column label="施工状态" min-width="200">
+    <div class="plan-dialog-content">
+      <div class="plan-toolbar">
+        <div>
+          <div class="plan-toolbar__title">计划明细</div>
+          <div class="plan-toolbar__subtitle">维护任务开始时间、结束时间及工作量</div>
+        </div>
+        <div class="plan-toolbar__actions">
+          <span class="plan-count">共 {{ planList.length }} 条</span>
+          <el-button size="default" type="primary" @click="addNewRow">
+            <Icon icon="ep:plus" class="mr-5px" />新增计划
+          </el-button>
+        </div>
+      </div>
+
+      <el-table :data="planList" border stripe max-height="50vh" class="plan-table">
+        <el-table-column label="序号" width="60" align="center">
+          <template #default="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <!-- <el-table-column label="施工状态" min-width="200">
         <template #default="scope">
           <el-select
             v-model="scope.row.status"
@@ -561,63 +585,70 @@ onMounted(async () => {
           </el-select>
         </template>
       </el-table-column> -->
-      <el-table-column label="开始时间" min-width="200">
-        <template #default="scope">
-          <el-date-picker
-            v-model="scope.row.startTime"
-            type="datetime"
-            placeholder="选择开始时间"
-            value-format="YYYY-MM-DD HH:mm"
-            format="YYYY-MM-DD HH:mm"
-            class="w-full" />
-        </template>
-      </el-table-column>
-      <el-table-column label="结束时间" min-width="200">
-        <template #default="scope">
-          <el-date-picker
-            v-if="rowShowEndTime(scope.row)"
-            v-model="scope.row.endTime"
-            type="datetime"
-            placeholder="选择结束时间"
-            value-format="YYYY-MM-DD HH:mm"
-            format="YYYY-MM-DD HH:mm"
-            class="w-full" />
-        </template>
-      </el-table-column>
-
-      <el-table-column
-        min-width="100"
-        v-for="item in currentTableColumns"
-        :key="item.identifier"
-        :prop="item.identifier">
-        <template #header>
-          <span>
-            <span v-if="isRequiredColumn(item)" class="required-mark">*</span>
-            {{ item.unit ? `${item.name}(${item.unit})` : item.name }}
-          </span>
-        </template>
-        <template #default="scope">
-          <!-- <el-input type="number" v-model="scope.row[item.identifier]" placeholder="请输入" /> -->
-          <el-input-number
-            style="width: 150px"
-            v-model="scope.row[item.identifier]"
-            :min="0"
-            placeholder="请输入" />
-        </template>
-      </el-table-column>
+        <el-table-column label="开始时间" min-width="200">
+          <template #default="scope">
+            <el-date-picker
+              v-model="scope.row.startTime"
+              type="datetime"
+              placeholder="选择开始时间"
+              value-format="YYYY-MM-DD HH:mm"
+              format="YYYY-MM-DD HH:mm"
+              class="w-full" />
+          </template>
+        </el-table-column>
+        <el-table-column label="结束时间" min-width="200">
+          <template #default="scope">
+            <el-date-picker
+              v-if="rowShowEndTime(scope.row)"
+              v-model="scope.row.endTime"
+              type="datetime"
+              placeholder="选择结束时间"
+              value-format="YYYY-MM-DD HH:mm"
+              format="YYYY-MM-DD HH:mm"
+              class="w-full" />
+          </template>
+        </el-table-column>
+
+        <el-table-column
+          min-width="100"
+          v-for="item in currentTableColumns"
+          :key="item.identifier"
+          :prop="item.identifier">
+          <template #header>
+            <span>
+              <span v-if="isRequiredColumn(item)" class="required-mark">*</span>
+              {{ item.unit ? `${item.name}(${item.unit})` : item.name }}
+            </span>
+          </template>
+          <template #default="scope">
+            <!-- <el-input type="number" v-model="scope.row[item.identifier]" placeholder="请输入" /> -->
+            <el-input-number
+              v-model="scope.row[item.identifier]"
+              size="default"
+              :min="0"
+              class="plan-number-input"
+              placeholder="请输入" />
+          </template>
+        </el-table-column>
 
-      <el-table-column label="操作" width="100" align="center">
-        <template #default="scope">
-          <el-button link type="danger" @click="removeRow(scope.$index)">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
+        <el-table-column label="操作" width="100" align="center">
+          <template #default="scope">
+            <el-button link type="danger" @click="removeRow(scope.$index)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
 
     <template #footer>
-      <span class="dialog-footer">
-        <el-button @click="planDialogVisible = false">取消</el-button>
-        <el-button type="primary" :loading="saveLoading" @click="savePlan">保存</el-button>
-      </span>
+      <div class="plan-dialog-footer">
+        <span>请确认各计划时间和工作量填写完整</span>
+        <div>
+          <el-button size="default" @click="planDialogVisible = false">取消</el-button>
+          <el-button size="default" type="primary" :loading="saveLoading" @click="savePlan">
+            <Icon icon="ep:check" class="mr-5px" />保存
+          </el-button>
+        </div>
+      </div>
     </template>
   </el-dialog>
 
@@ -709,6 +740,138 @@ onMounted(async () => {
   margin-bottom: 0;
 }
 
+.plan-dialog-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  padding-right: 40px;
+}
+
+.plan-dialog-title__icon {
+  display: inline-flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 40px;
+  height: 40px;
+  margin-right: 12px;
+  color: #3478f6;
+  background: #eaf2ff;
+  border-radius: 8px;
+}
+
+.plan-dialog-title__content {
+  min-width: 0;
+}
+
+.plan-dialog-title h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 24px;
+  color: var(--el-text-color-primary);
+}
+
+.plan-dialog-title p {
+  margin: 2px 0 0;
+  overflow: hidden;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plan-dialog-title__separator {
+  margin: 0 6px;
+  color: var(--el-text-color-placeholder);
+}
+
+.plan-dialog :deep(.el-dialog__header) {
+  padding: 16px 20px;
+  margin-right: 0;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.plan-dialog :deep(.el-dialog__body) {
+  padding: 16px 20px;
+}
+
+.plan-dialog :deep(.el-dialog__footer) {
+  padding: 12px 20px;
+  border-top: 1px solid var(--el-border-color-lighter);
+}
+
+.plan-dialog-content {
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+}
+
+.plan-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 60px;
+  padding: 10px 14px;
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.plan-toolbar__title {
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.plan-toolbar__subtitle {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.plan-toolbar__actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.plan-count {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
+
+.plan-table {
+  width: 100%;
+}
+
+.plan-table :deep(.el-table__cell) {
+  padding: 8px 0;
+}
+
+.plan-number-input {
+  width: 100%;
+}
+
+.plan-dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.plan-dialog-footer > span {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+.plan-dialog-footer :deep(.el-button) {
+  margin-left: 8px;
+}
+
 .required-mark {
   margin-right: 2px;
   color: var(--el-color-danger);
@@ -779,6 +942,24 @@ onMounted(async () => {
     width: 100%;
   }
 
+  .plan-dialog .plan-dialog-title p,
+  .plan-dialog .plan-toolbar__subtitle,
+  .plan-dialog .plan-dialog-footer > span {
+    display: none;
+  }
+
+  .plan-dialog .plan-toolbar {
+    align-items: flex-start;
+  }
+
+  .plan-dialog .plan-toolbar__actions {
+    gap: 8px;
+  }
+
+  .plan-dialog .plan-dialog-footer {
+    justify-content: flex-end;
+  }
+
   .query-row {
     grid-template-columns: minmax(0, 1fr);
   }

Разлика између датотеке није приказан због своје велике величине
+ 512 - 440
src/views/pms/maintain/IotMaintainAddEdit.vue


+ 417 - 222
src/views/pms/maintain/IotMaintainDetail.vue

@@ -1,308 +1,347 @@
 <template>
-  <ContentWrap v-loading="formLoading">
+  <div v-loading="formLoading" class="maintain-detail-page">
+    <div class="detail-header">
+      <div class="detail-header__main">
+        <div class="detail-header__icon">
+          <Icon icon="ep:tools" />
+        </div>
+        <div>
+          <h2 class="detail-title">维修工单详情</h2>
+          <p class="detail-subtitle">查看设备故障、维修处理与关联物料信息</p>
+        </div>
+      </div>
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </div>
+
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formData.type === 'in' ? formRules : outFormRules"
-      v-loading="formLoading"
-      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-      label-width="130px"
-    >
-      <div class="base-expandable-content">
-        <el-row>
-          <el-col :span="8">
+      size="default"
+      label-width="112px"
+      class="detail-form">
+      <section class="detail-section">
+        <div class="section-heading"><span></span>基础信息</div>
+        <el-row :gutter="24">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item :label="t('maintain.deviceName')" prop="deviceName">
-              <el-select disabled v-model="formData.deviceName" />
+              <el-input v-model="formData.deviceName" disabled />
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item :label="t('maintain.status')" prop="status">
-              <el-select v-model="formData.status" clearable disabled>
+              <el-select v-model="formData.status" disabled>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAIN_STATUS)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item :label="t('iotMaintain.repairType')" prop="type">
-              <el-select disabled v-model="formData.type" clearable>
+              <el-select v-model="formData.type" disabled>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAIN_TYPE)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item :label="t('iotMaintain.shutDown')" prop="ifStop">
-              <el-select v-model="formData.ifStop" clearable disabled>
+              <el-select v-model="formData.ifStop" disabled>
                 <el-option
                   v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.failureTime')" prop="failureTime">
+              <el-date-picker
+                v-model="formData.failureTime"
+                type="datetime"
+                value-format="x"
+                disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.faultySystem')" prop="failureSystem">
+              <el-input v-model="formData.failureSystem" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.failureImpact')" prop="failureInfluence">
+              <el-input v-model="formData.failureInfluence" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </section>
+
+      <section class="detail-section">
+        <div class="section-heading"><span></span>维修处理</div>
+        <el-row :gutter="24">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item
               :label="t('iotMaintain.startTime')"
               prop="maintainStartTime"
-              :rules="formData.type === 'in' ? startRule : []"
-            >
+              :rules="formData.type === 'in' ? startRule : []">
               <el-date-picker
-                disabled
-                style="width: 150%"
                 v-model="formData.maintainStartTime"
                 type="datetime"
                 value-format="x"
-              />
+                disabled />
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item
               :label="t('iotMaintain.endTime')"
               prop="maintainEndTime"
-              :rules="formData.type === 'in' ? endRule : []"
-            >
+              :rules="formData.type === 'in' ? endRule : []">
               <el-date-picker
-                disabled
-                style="width: 150%"
                 v-model="formData.maintainEndTime"
                 type="datetime"
                 value-format="x"
-              />
+                disabled />
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item :label="t('iotMaintain.PersonInCharge')" prop="maintainPerson">
-              <el-select
-                v-model="formData.maintainPerson"
-                filterable
-                clearable
-                style="width: 100%"
-                disabled
-              >
-                <el-option
-                  v-for="item in deptUsers"
-                  :key="item.id"
-                  :label="item.nickname"
-                  :value="item.id"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.failureTime')" prop="failureTime">
-              <el-date-picker
-                disabled
-                style="width: 150%"
-                v-model="formData.failureTime"
-                type="datetime"
-                value-format="x"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.failureImpact')" prop="failureInfluence">
-              <el-input v-model="formData.failureInfluence" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.faultySystem')" prop="failureSystem">
-              <el-input v-model="formData.failureSystem" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.picture')" prop="pics">
-              <UploadImgs v-model="formData.pics" :disabled="true" height="55px" />
+              <el-input v-model="formData.maintainPerson" disabled />
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
             <el-form-item :label="t('maintain.maintainWay')" prop="maintainMethod">
-              <el-select v-model="formData.maintainMethod" clearable disabled>
+              <el-select v-model="formData.maintainMethod" disabled>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAINTAIN_METHOD)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="8">
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item
+              :label="t('iotMaintain.repairCosts')"
+              prop="maintainFee"
+              :rules="formData.type === 'out' ? feeRules : []">
+              <el-input-number
+                v-model="formData.maintainFee"
+                :min="0"
+                :precision="2"
+                :step="0.1"
+                controls-position="right"
+                disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
             <el-form-item :label="t('maintain.methodDescription')" prop="methodDescription">
-              <el-input v-model="formData.methodDescription" type="textarea"  disabled />
+              <el-input
+                v-model="formData.methodDescription"
+                type="textarea"
+                :rows="2"
+                resize="none"
+                disabled />
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('iotMaintain.applyPerson')" prop="applyPersonId" :rules="formData.type==='out'?classifyRules:[]">
-              <el-select v-model="formData.applyPersonId" :placeholder="t('iotMaintain.applyPersonHolder')" disabled clearable>
+        </el-row>
+      </section>
+
+      <section v-if="formData.type === 'out'" class="detail-section">
+        <div class="section-heading"><span></span>外委信息</div>
+        <el-row :gutter="24">
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item
+              :label="t('iotMaintain.applyPerson')"
+              prop="applyPersonId"
+              :rules="classifyRules">
+              <el-select v-model="formData.applyPersonId" disabled>
                 <el-option
                   v-for="person in applyPeoPle"
                   :key="person.oaId"
                   :label="person.lastname"
-                  :value="person.oaId"
-                />
+                  :value="person.oaId" />
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('iotMaintain.maintainClassify')" prop="maintainClassify" :rules="formData.type==='out'?classifyRules:[]">
-              <el-select v-model="formData.maintainClassify" :placeholder="t('iotMaintain.maintainClassifyHolder')" disabled clearable>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item
+              :label="t('iotMaintain.maintainClassify')"
+              prop="maintainClassify"
+              :rules="classifyRules">
+              <el-select v-model="formData.maintainClassify" disabled>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAIN_CLASSIFY)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('iotMaintain.kmHour')" prop="kmHour">
-              <el-input v-model="formData.kmHour" :placeholder="t('iotMaintain.kmHourHolder')" disabled/>
-            </el-form-item>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.kmHour')" prop="kmHour"
+              ><el-input v-model="formData.kmHour" disabled
+            /></el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('deviceForm.model')" prop="model" :rules="formData.type==='out'?modelRules:[]">
-              <el-input v-model="formData.model" :placeholder="t('deviceForm.modelHolder')" disabled/>
-            </el-form-item>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('deviceForm.model')" prop="model" :rules="modelRules"
+              ><el-input v-model="formData.model" disabled
+            /></el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('deviceForm.enable')" prop="enableDate" :rules="formData.type==='out'?modelRules:[]">
-              <el-date-picker
-                disabled
-                style="width: 150%"
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('deviceForm.enable')" prop="enableDate" :rules="modelRules"
+              ><el-date-picker
                 v-model="formData.enableDate"
                 type="date"
                 value-format="YYYY-MM-DD"
-                :placeholder="t('deviceForm.enableHolder')"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <div style="display: flex;flex-direction: row">
-              <el-form-item :label="t('iotMaintain.supplier')" prop="supplier" style="width: 86%" :rules="formData.type==='out'?supplierRules:[]">
-                <el-input
-                  disabled
-                  clearable
-                  v-model="formData.supplier"
-                  :placeholder="t('iotMaintain.suppHolder')"
-                />
-              </el-form-item>
-            </div>
-          </el-col>
-          <el-col :span="8" >
-            <el-form-item :label="t('iotMaintain.repairCosts')" prop="maintainFee" :rules="formData.type==='out'?feeRules:[]">
-              <el-input-number
                 disabled
-                v-model="formData.maintainFee"
-                :min="0"
-                :precision="2"
-                :step="0.1"
-                style="width: 100%"
-              />
-            </el-form-item>
+            /></el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('iotMaintain.projectManager')" prop="projectManager" :rules="formData.type==='out'?projectRules:[]">
-              <el-select v-model="formData.projectManager" :placeholder="t('iotMaintain.projectManagerHolder')" disabled clearable filterable>
-                <el-option
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.supplier')" prop="supplier" :rules="supplierRules"
+              ><el-input v-model="formData.supplier" disabled
+            /></el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item
+              :label="t('iotMaintain.projectManager')"
+              prop="projectManager"
+              :rules="projectRules">
+              <el-select v-model="formData.projectManager" disabled
+                ><el-option
                   v-for="person in projectManager"
                   :key="person.oaId"
                   :label="person.lastname"
                   :value="person.oaId"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8"  v-if="formData.type === 'out'">
-            <el-form-item :label="t('iotMaintain.address')" prop="address">
-              <el-input  v-model="formData.address" disabled/>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'">
-            <el-form-item :label="t('iotMaintain.attachment')" prop="outFiles">
-              <UploadFile v-model="formData.outFiles" :is-show-tip="false" class="min-w-80px" multiple :limit="2" disabled />
+              /></el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="16"  v-if="formData.type === 'out'" >
-            <el-form-item :label="t('iotMaintain.maintainItem')" prop="maintainItem" :rules="formData.type==='out'?itemRules:[]">
-              <el-input type="textarea" v-model="formData.maintainItem" disabled/>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.faultDescription')" prop="description">
-              <el-input type="textarea" v-model="formData.description" disabled />
-            </el-form-item>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.address')" prop="address"
+              ><el-input v-model="formData.address" disabled
+            /></el-form-item>
           </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.repairDescription')" prop="maintainDescription">
-              <el-input type="textarea" v-model="formData.maintainDescription" disabled />
-            </el-form-item>
+          <el-col :xs="24" :sm="12" :xl="8">
+            <el-form-item :label="t('iotMaintain.attachment')" prop="outFiles"
+              ><UploadFile
+                v-model="formData.outFiles"
+                :is-show-tip="false"
+                multiple
+                :limit="2"
+                disabled
+            /></el-form-item>
           </el-col>
-          <el-col :span="8">
-            <el-form-item :label="t('iotMaintain.solution')" prop="solution">
-              <el-input type="textarea" v-model="formData.solution" disabled />
-            </el-form-item>
+          <el-col :span="24">
+            <el-form-item
+              :label="t('iotMaintain.maintainItem')"
+              prop="maintainItem"
+              :rules="itemRules"
+              ><el-input
+                v-model="formData.maintainItem"
+                type="textarea"
+                :rows="2"
+                resize="none"
+                disabled
+            /></el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item :label="t('iotMaintain.remark')" prop="remark">
-              <el-input v-model="formData.remark" type="textarea" disabled />
+        </el-row>
+      </section>
+
+      <section class="detail-section">
+        <div class="section-heading"><span></span>故障描述</div>
+        <el-row :gutter="24">
+          <el-col :xs="24" :xl="8">
+            <el-form-item :label="t('iotMaintain.picture')" prop="pics">
+              <UploadImgs v-model="formData.pics" :disabled="true" height="72px" />
             </el-form-item>
           </el-col>
+          <el-col :xs="24" :xl="16"
+            ><el-form-item :label="t('iotMaintain.faultDescription')" prop="description"
+              ><el-input
+                v-model="formData.description"
+                type="textarea"
+                :rows="3"
+                resize="none"
+                disabled /></el-form-item
+          ></el-col>
+          <el-col :xs="24" :xl="12"
+            ><el-form-item :label="t('iotMaintain.repairDescription')" prop="maintainDescription"
+              ><el-input
+                v-model="formData.maintainDescription"
+                type="textarea"
+                :rows="3"
+                resize="none"
+                disabled /></el-form-item
+          ></el-col>
+          <el-col :xs="24" :xl="12"
+            ><el-form-item :label="t('iotMaintain.solution')" prop="solution"
+              ><el-input
+                v-model="formData.solution"
+                type="textarea"
+                :rows="3"
+                resize="none"
+                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"
+                resize="none"
+                disabled /></el-form-item
+          ></el-col>
         </el-row>
-      </div>
+      </section>
     </el-form>
-  </ContentWrap>
-  <ContentWrap>
-    <!-- 列表 -->
-    <ContentWrap>
-      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column :label="t('iotMaintain.deviceCode')" align="center" prop="deviceCode" />
-        <el-table-column :label="t('iotMaintain.deviceName')" align="center" prop="deviceName" />
-        <el-table-column :label="t('iotMaintain.repairItems')" align="center" prop="name" />
-
-        <el-table-column :label="t('iotMaintain.operation')" align="center" min-width="120px">
+
+    <section class="detail-section material-section">
+      <div class="section-heading"><span></span>关联物料</div>
+      <ZmTable
+        :loading="formLoading"
+        :data="list"
+        :show-border="true"
+        settings-cache-key="pms-maintain-detail-material">
+        <ZmTableColumn
+          :label="t('iotMaintain.deviceCode')"
+          align="center"
+          prop="deviceCode"
+          fixed="left" />
+        <ZmTableColumn :label="t('iotMaintain.deviceName')" align="center" prop="deviceName" />
+        <ZmTableColumn :label="t('iotMaintain.repairItems')" align="center" prop="name" />
+        <ZmTableColumn :label="t('iotMaintain.operation')" align="center" width="120" fixed="right">
           <template #default="scope">
-            <div style="display: flex; justify-content: center; align-items: center; width: 100%">
-              <div style="margin-left: 12px">
-                <el-button
-                  link
-                  type="primary"
-                  @click="handleViewNew(scope.row.id, scope.row.bomNodeId)"
-                >
-                  物料详情
-                </el-button>
-              </div>
-            </div>
+            <el-button
+              size="default"
+              link
+              type="primary"
+              @click="handleViewNew(scope.row.id, scope.row.bomNodeId)"
+              >物料详情</el-button
+            >
           </template>
-        </el-table-column>
-      </el-table>
-    </ContentWrap>
-  </ContentWrap>
-  <ContentWrap>
-    <el-form>
-      <el-form-item style="float: right">
-        <el-button type="info" @click="close">取 消</el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
+        </ZmTableColumn>
+      </ZmTable>
+    </section>
+
+    <div class="sticky-action-bar">
+      <el-button size="default" @click="close">返回</el-button>
+    </div>
+  </div>
+
   <MaterialListDrawer
     :model-value="drawerVisible"
     @update:model-value="(val) => (drawerVisible = val)"
     :node-id="currentBomNodeId"
     :materials="materialList"
-    :detail="true"
-  />
+    :detail="true" />
 </template>
 <script setup lang="ts">
 import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
@@ -313,12 +352,13 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
 import { IotMaintainApi } from '@/api/pms/maintain'
 import MaterialListDrawer from '@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue'
 import { IotMaintainMaterialsApi } from '@/api/pms/maintain/materials'
-import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 
 /** 维修工单 表单 */
 defineOptions({ name: 'IotMaintainDetail' })
 
 const { t } = useI18n() // 国际化
+const { ZmTable, ZmTableColumn } = useTableComponents<IotMaintainMaterialVO>()
 
 const projectRules = [{ required: true, message: '项目经理不能为空', trigger: 'blur' }]
 const itemRules = [{ required: true, message: '维修项目不能为空', trigger: 'blur' }]
@@ -331,32 +371,26 @@ const applyPeoPle = ref([])
 const projectManager = ref([])
 
 const props = defineProps<{ id?: number }>()
-const message = useMessage() // 消息弹窗
 const { delView } = useTagsViewStore() // 视图操作
 const { currentRoute, push } = useRouter()
-const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
-const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
 const drawerVisible = ref<boolean>(false)
-const showDrawer = ref()
 const list = ref<IotMaintainMaterialVO[]>([]) // 列表的数据
-const { params, name } = useRoute() // 查询参数
+const { params } = useRoute() // 查询参数
 const id = params.id || props.id
 const materialList = ref<any[]>([]) // 保养工单bom关联物料列表
-const { wsCache } = useCache()
 const currentBomNodeId = ref<string>()
 const formData = ref({
   methodDescription: undefined,
   maintainMethod: undefined,
-  kmHour:undefined,
-  maintainItem:undefined,
+  kmHour: undefined,
+  maintainItem: undefined,
   enableDate: undefined,
-  projectManager:undefined,
-  applyPersonId:undefined,
+  projectManager: undefined,
+  applyPersonId: undefined,
   maintainClassify: undefined,
-  address:undefined,
-  model:undefined,
+  address: undefined,
+  model: undefined,
   id: undefined,
   failureCode: undefined,
   failureName: undefined,
@@ -369,8 +403,8 @@ const formData = ref({
   failureSystem: undefined,
   description: undefined,
   pic: undefined,
-  pics:undefined,
-  outFiles:undefined,
+  pics: undefined,
+  outFiles: undefined,
   solution: undefined,
   maintainStartTime: undefined,
   maintainEndTime: undefined,
@@ -384,7 +418,7 @@ const formData = ref({
   supplier: undefined,
   maintainFee: undefined,
   outFile: undefined,
-  supplierName: undefined,
+  supplierName: undefined
 })
 const formRules = reactive({
   // deviceName: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
@@ -437,13 +471,10 @@ const handleViewNew = (nodeId, bomId) => {
     materialList.value = res.list
   })
 }
-const userId = ref('')
 /** 提交表单 */
 onMounted(async () => {
-  const userInfo = wsCache.get(CACHE_KEY.USER)
-  userId.value = userInfo.user.nickname;
+  formLoading.value = true
   await IotMaintainApi.getIotMaintain(id).then((res) => {
-    debugger
     formData.value = res
     if (res.outFile) {
       formData.value.outFile = res.outFile.split(',')
@@ -462,10 +493,174 @@ onMounted(async () => {
         applyPeoPle.value = resss
       })
       IotMaintainApi.getProjectUsers(res.creator).then((ressss) => {
-        debugger
         projectManager.value = ressss
       })
     }
   })
+  formLoading.value = false
 })
 </script>
+
+<style scoped>
+.maintain-detail-page {
+  display: flex;
+  flex-direction: column;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  padding: 16px;
+  background: #f5f7fa;
+}
+
+.detail-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  margin-bottom: 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
+}
+
+.detail-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.detail-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 42px;
+  height: 42px;
+  margin-right: 12px;
+  font-size: 20px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.detail-title {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  line-height: 26px;
+  color: var(--el-text-color-primary);
+}
+
+.detail-subtitle {
+  margin: 2px 0 0;
+  font-size: 13px;
+  line-height: 20px;
+  color: var(--el-text-color-secondary);
+}
+
+.detail-form,
+.detail-section {
+  min-width: 0;
+}
+
+.detail-section {
+  padding: 0 20px 4px;
+  margin-bottom: 16px;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
+}
+
+.section-heading {
+  display: flex;
+  height: 46px;
+  padding: 0 20px;
+  margin: 0 -20px 16px;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  align-items: center;
+}
+
+.section-heading span {
+  width: 4px;
+  height: 18px;
+  margin-right: 10px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.detail-form :deep(.el-select),
+.detail-form :deep(.el-date-editor),
+.detail-form :deep(.el-input-number) {
+  width: 100%;
+}
+
+.detail-form :deep(.el-form-item) {
+  margin-bottom: 16px;
+}
+
+.material-section {
+  padding-bottom: 20px;
+}
+
+.sticky-action-bar {
+  position: sticky;
+  bottom: 0;
+  z-index: 20;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10px;
+  min-height: 58px;
+  padding: 10px 20px;
+  margin-top: auto;
+  background: var(--el-bg-color);
+  border-top: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 -4px 12px rgb(0 0 0 / 8%);
+}
+
+.sticky-action-bar :deep(.el-button) {
+  margin-left: 0;
+}
+
+@media (width <= 768px) {
+  .maintain-detail-page {
+    padding: 12px;
+  }
+
+  .sticky-action-bar {
+    padding-right: 12px;
+    padding-left: 12px;
+    margin-right: -12px;
+    margin-bottom: -12px;
+    margin-left: -12px;
+  }
+
+  .detail-header,
+  .detail-section {
+    padding-right: 12px;
+    padding-left: 12px;
+  }
+
+  .detail-subtitle {
+    display: none;
+  }
+
+  .section-heading {
+    padding-right: 12px;
+    padding-left: 12px;
+    margin-right: -12px;
+    margin-left: -12px;
+  }
+
+  .detail-form {
+    --el-form-label-width: 96px;
+  }
+}
+</style>

+ 417 - 188
src/views/pms/maintain/IotMaintainRecord.vue

@@ -1,158 +1,150 @@
 <template>
-  <ContentWrap v-loading="formLoading">
-    <!-- 第一部分:日报标题 -->
-    <div class="daily-report-title">
-      <h2>{{ pageTitle }}</h2>
-    </div>
-
-    <!-- 第二部分:项目/任务信息 -->
-    <ContentWrap>
-      <div class="info-table" style="margin-top: 0.5em">
-        <!-- 表格行 -->
-        <div class="table-row">
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">油服编码:</span>
-              <!-- 甲方字段:添加 single-line-ellipsis 类 + title 绑定完整内容 -->
-              <span
-                class="cell-value single-line-ellipsis"
-                :title="repairInfo.yfDeviceCode || repairInfo.deviceCode || '-'"
-              >
-                {{ repairInfo.yfDeviceCode || repairInfo.deviceCode || '-' }}
-              </span>
-            </div>
-          </div>
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">设备名称:</span>
-              <span class="cell-value">{{ repairInfo.deviceName || '-' }}</span>
-            </div>
-          </div>
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">维修类型:</span>
-              <span class="cell-value">{{
-                getDictLabel(DICT_TYPE.PMS_MAIN_TYPE, repairInfo.type)
-              }}</span>
-            </div>
-          </div>
+  <div v-loading="formLoading" class="maintain-record-page">
+    <div class="page-header">
+      <div class="page-header__main">
+        <div class="page-header__icon"><Icon icon="ep:document" /></div>
+        <div>
+          <h2 class="page-title">{{ pageTitle }}</h2>
+          <p class="page-subtitle">查看设备维修信息并维护维修过程记录</p>
         </div>
+      </div>
+      <el-button size="default" @click="close">
+        <Icon icon="ep:arrow-left" class="mr-5px" />返回
+      </el-button>
+    </div>
 
-        <div class="table-row">
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">维修开始时间:</span>
-              <span class="cell-value">{{ formatDate(repairInfo.maintainStartTime) || '-' }}</span>
-            </div>
-          </div>
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">维修结束日期:</span>
-              <span class="cell-value">{{ formatDate(repairInfo.maintainEndTime) || '-' }}</span>
-            </div>
-          </div>
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">故障时间:</span>
-              <span class="cell-value">{{ formatDate(repairInfo.failureTime) || '-' }}</span>
-            </div>
-          </div>
-        </div>
-        <div class="table-row">
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">维修人员:</span>
-              <span class="cell-value">{{ repairInfo.maintainPersonName || '-' }}</span>
-            </div>
-          </div>
-          <div class="table-cell">
-            <div class="cell-content">
-              <span class="cell-label">故障系统:</span>
-              <span class="cell-value">{{ repairInfo.failureSystem || '-' }}</span>
-            </div>
-          </div>
-          <div class="table-cell">
-            <div class="cell-content">
-              <!--              <span class="cell-label">故障时间:</span>-->
-              <!--              <span class="cell-value">{{ formatDate(repairInfo.failureTime) || '-' }}</span>-->
-            </div>
-          </div>
+    <section class="page-section info-section">
+      <div class="section-header">
+        <div class="section-title"><span></span>工单概览</div>
+      </div>
+      <div class="info-grid">
+        <div class="info-item">
+          <span class="info-label">油服编码</span>
+          <span class="info-value" :title="repairInfo.yfDeviceCode || repairInfo.deviceCode || '-'">
+            {{ repairInfo.yfDeviceCode || repairInfo.deviceCode || '-' }}
+          </span>
         </div>
+        <div class="info-item"
+          ><span class="info-label">设备名称</span
+          ><span class="info-value">{{ repairInfo.deviceName || '-' }}</span></div
+        >
+        <div class="info-item"
+          ><span class="info-label">维修类型</span
+          ><span class="info-value">{{
+            getDictLabel(DICT_TYPE.PMS_MAIN_TYPE, repairInfo.type) || '-'
+          }}</span></div
+        >
+        <div class="info-item"
+          ><span class="info-label">维修开始时间</span
+          ><span class="info-value">{{
+            formatDate(repairInfo.maintainStartTime) || '-'
+          }}</span></div
+        >
+        <div class="info-item"
+          ><span class="info-label">维修结束日期</span
+          ><span class="info-value">{{ formatDate(repairInfo.maintainEndTime) || '-' }}</span></div
+        >
+        <div class="info-item"
+          ><span class="info-label">故障时间</span
+          ><span class="info-value">{{ formatDate(repairInfo.failureTime) || '-' }}</span></div
+        >
+        <div class="info-item"
+          ><span class="info-label">维修人员</span
+          ><span class="info-value">{{ repairInfo.maintainPersonName || '-' }}</span></div
+        >
+        <div class="info-item"
+          ><span class="info-label">故障系统</span
+          ><span class="info-value">{{ repairInfo.failureSystem || '-' }}</span></div
+        >
+        <div class="info-item info-item--empty" aria-hidden="true"></div>
       </div>
-    </ContentWrap>
-    <ContentWrap>
-      <el-form :inline="true" ref="queryFormRef" :model="queryParams">
-        <el-form-item>
-          <el-form-item label="维修时间" prop="createTime">
-            <el-date-picker
-              v-model="queryParams.repairTime"
-              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-220px"
-            />
-          </el-form-item>
-        </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" @click="openForm('create')">
-            <Icon icon="ep:plus" /> 新增
+    </section>
+
+    <section class="page-section records-section">
+      <div class="section-header">
+        <div class="section-title"><span></span>维修记录</div>
+        <div class="section-actions">
+          <el-button size="default" type="primary" @click="openForm('create')">
+            <Icon icon="ep:plus" class="mr-5px" />新增
           </el-button>
-          <el-button type="warning" @click="viewTimeline">
-            <Icon icon="ep:timer" /> 时间线
+          <el-button size="default" type="warning" @click="viewTimeline">
+            <Icon icon="ep:timer" class="mr-5px" />时间线
           </el-button>
+        </div>
+      </div>
+
+      <el-form ref="queryFormRef" :model="queryParams" size="default" class="query-form">
+        <el-form-item label="维修时间" prop="repairTime">
+          <el-date-picker
+            v-model="queryParams.repairTime"
+            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="query-date" />
+        </el-form-item>
+        <el-form-item class="query-actions">
+          <el-button size="default" type="primary" @click="handleQuery"
+            ><Icon icon="ep:search" class="mr-5px" />搜索</el-button
+          >
+          <el-button size="default" @click="resetQuery"
+            ><Icon icon="ep:refresh" class="mr-5px" />重置</el-button
+          >
         </el-form-item>
       </el-form>
-    </ContentWrap>
-    <ContentWrap>
-      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column
-          label="维修时间"
-          align="center"
-          prop="repairTime"
-          :formatter="dateFormatter"
-          width="150px"
-        />
-        <el-table-column label="维修项" align="center" min-width="90" prop="repairItem" />
-        <el-table-column label="维修内容" align="center" min-width="150" prop="repairContent" />
-        <el-table-column label="解决办法" align="center" prop="solution" min-width="150" />
-        <el-table-column label="图片" align="center"  prop="pic" />
-        <el-table-column label="创建人" align="center" prop="createName" />
-        <el-table-column label="配合维修人员" align="center" prop="repairPersonName" />
-        <el-table-column label="备注" align="center" prop="remark" />
-        <el-table-column
-          label="创建时间"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-          width="180px"
-        />
-        <el-table-column label="操作" align="center" min-width="90px">
-          <template #default="scope">
-            <el-button link type="primary" v-if="userid.toString()===scope.row.creator" @click="openForm('update', scope.row.id)">
-              编辑
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <!-- 分页 -->
+
+      <div class="table-wrap">
+        <ZmTable
+          :loading="loading"
+          :data="list"
+          :show-border="true"
+          settings-cache-key="pms-maintain-record-list">
+          <ZmTableColumn
+            label="维修时间"
+            prop="repairTime"
+            :formatter="dateFormatter"
+            min-width="150" />
+          <ZmTableColumn label="维修项" prop="repairItem" />
+          <ZmTableColumn label="维修内容" prop="repairContent" min-width="150" />
+          <ZmTableColumn label="解决办法" prop="solution" min-width="150" />
+          <ZmTableColumn label="图片" prop="pic" />
+          <ZmTableColumn label="创建人" prop="createName" />
+          <ZmTableColumn label="配合维修人员" prop="repairPersonName" />
+          <ZmTableColumn label="备注" prop="remark" />
+          <ZmTableColumn
+            label="创建时间"
+            prop="createTime"
+            :formatter="dateFormatter"
+            min-width="180" />
+          <ZmTableColumn label="操作" width="90" fixed="right">
+            <template #default="scope">
+              <el-button
+                v-if="userid.toString() === scope.row.creator"
+                size="default"
+                link
+                type="primary"
+                @click="openForm('update', scope.row.id)">
+                编辑
+              </el-button>
+            </template>
+          </ZmTableColumn>
+        </ZmTable>
+      </div>
+
       <Pagination
         :total="total"
         v-model:page="queryParams.pageNo"
         v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </ContentWrap>
+        @pagination="getList" />
+    </section>
+
     <IotJdRecordForm ref="formRef" @success="getList" />
     <MaintainRecordDrawer
       ref="showDrawer"
       :model-value="drawerVisible"
-      @update:model-value="(val) => (drawerVisible = val)"
-    />
-  </ContentWrap>
+      @update:model-value="(val) => (drawerVisible = val)" />
+  </div>
 </template>
 
 <script setup lang="ts">
@@ -169,6 +161,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import { IotJdRecordApi, IotJdRecordVO } from '@/api/pms/maintain/record'
 import IotJdRecordForm from '@/views/pms/maintain/record/IotJdRecordForm.vue'
 import MaintainRecordDrawer from '@/views/pms/maintain/MaintainRecordDrawer.vue'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 
 const { t } = useI18n()
 const message = useMessage()
@@ -176,8 +169,9 @@ const { delView } = useTagsViewStore()
 const { push, currentRoute } = useRouter()
 const { params } = useRoute()
 const userStore = useUserStore()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotJdRecordVO>()
 const total = ref(0) // 列表的总页数
-const list = ref<IotJdRecordVO>([])
+const list = ref<IotJdRecordVO[]>([])
 const loading = ref(true) // 列表的加载中
 const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
@@ -243,7 +237,7 @@ const pageTitle = computed(() => {
 
 const close = () => {
   delView(unref(currentRoute))
-  push({ name: 'FillDailyReport', params: {} })
+  push({ name: 'IotMaintain', params: {} })
 }
 
 const formatDate = (timestamp: number) => {
@@ -271,10 +265,81 @@ onMounted(async () => {
 </script>
 
 <style scoped>
+/* 响应式设计 */
+@media (width <= 768px) {
+  .table-row {
+    flex-direction: column;
+  }
+
+  .table-cell {
+    border-right: none;
+    border-bottom: 1px solid #e0e0e0;
+  }
+
+  .table-cell:last-child {
+    border-bottom: none;
+  }
+}
+
+@media (width <= 1200px) {
+  .info-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .info-item--empty {
+    display: none;
+  }
+}
+
+@media (width <= 768px) {
+  .maintain-record-page {
+    padding: 12px;
+  }
+
+  .page-header,
+  .section-header {
+    padding-right: 12px;
+    padding-left: 12px;
+  }
+
+  .page-subtitle {
+    display: none;
+  }
+
+  .section-header {
+    align-items: flex-start;
+    flex-direction: column;
+    padding-top: 12px;
+    padding-bottom: 12px;
+  }
+
+  .section-actions {
+    justify-content: flex-start;
+    width: 100%;
+  }
+
+  .info-grid {
+    grid-template-columns: minmax(0, 1fr);
+    margin: 12px;
+  }
+
+  .query-form,
+  .table-wrap {
+    padding-right: 12px;
+    padding-left: 12px;
+  }
+
+  .query-form,
+  .query-form :deep(.el-form-item),
+  .query-date {
+    width: 100%;
+  }
+}
+
 .info-table {
+  overflow: hidden;
   border: 1px solid #e0e0e0;
   border-radius: 4px;
-  overflow: hidden;
 }
 
 .table-row {
@@ -287,11 +352,11 @@ onMounted(async () => {
 }
 
 .table-cell {
-  flex: 1;
-  border-right: 1px solid #e0e0e0;
-  padding: 12px 8px;
-  min-height: 44px;
   display: flex;
+  min-height: 44px;
+  padding: 12px 8px;
+  border-right: 1px solid #e0e0e0;
+  flex: 1;
   align-items: center;
 }
 
@@ -311,89 +376,81 @@ onMounted(async () => {
 }
 
 .cell-label {
-  font-weight: 500;
+  min-width: 80px;
+  margin-right: 8px;
+
   /* 统一字体大小为 14px(Element 表单默认) */
   font-size: 14px;
+  font-weight: 500;
   color: #606266;
-  min-width: 80px;
-  margin-right: 8px;
   flex-shrink: 0;
 }
 
 .cell-value {
   /* 统一字体大小为 14px(Element 输入框默认) */
   font-size: 14px;
-  color: #303133;
+
   /* 统一行高为 1.5(Element 组件默认行高) */
   line-height: 1.5;
-  flex: 1;
+  color: #303133;
   word-break: break-all;
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
-  .table-row {
-    flex-direction: column;
-  }
-
-  .table-cell {
-    border-right: none;
-    border-bottom: 1px solid #e0e0e0;
-  }
-
-  .table-cell:last-child {
-    border-bottom: none;
-  }
+  flex: 1;
 }
 
 .daily-report-title {
-  text-align: center;
-  margin: 5px 0;
   padding: 5px;
+  margin: 5px 0;
+  text-align: center;
   border-bottom: 2px solid #409eff;
 }
 
 .daily-report-title h2 {
   margin: 0;
-  color: #303133;
   font-size: 16px;
   font-weight: bold;
+  color: #303133;
 }
 
 /* 为第二、三部分增加左右留白 */
 .section-padding {
-  padding-left: 0px;
   padding-right: 40px;
+  padding-left: 0;
 }
+
 :deep(.el-textarea .el-textarea__inner) {
   min-height: 80px;
 }
 
 /* 确保表单label不换行 */
 :deep(.el-form-item__label) {
-  white-space: nowrap;
-  text-overflow: ellipsis;
   overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 /* 甲方字段:单行显示+超出省略 */
 .single-line-ellipsis {
-  /* 强制文本单行显示 */
-  white-space: nowrap;
   /* 超出容器部分隐藏 */
   overflow: hidden;
+
   /* 超出部分显示省略号 */
   text-overflow: ellipsis;
+
   /* 避免文本被截断(可选,根据需求调整) */
   word-break: normal;
+
+  /* 强制文本单行显示 */
+  white-space: nowrap;
 }
 
 /* 设备配置字段:换行缩进(与首行对齐) */
 .indent-multiline {
   /* 首行及换行后缩进 2em(与 label 宽度匹配,可根据需求调整) */
-  text-indent: 0em;
+  text-indent: 0;
+
   /* 允许长文本换行(覆盖原有 cell-value 的 break-all,确保中文换行正常) */
   word-break: break-word;
+
   /* 保证换行后文本正常显示(可选,清除可能的 nowrap 影响) */
   white-space: normal;
 }
@@ -401,24 +458,24 @@ onMounted(async () => {
 /* 审批模式下表单字段的只读样式 */
 :deep(.el-form-item.is-disabled .el-input__inner),
 :deep(.el-form-item.is-disabled .el-textarea__inner) {
-  background-color: #f5f7fa;
-  border-color: #e4e7ed;
   color: #c0c4cc;
   cursor: not-allowed;
+  background-color: #f5f7fa;
+  border-color: #e4e7ed;
 }
 
 :deep(.el-form-item.is-disabled .el-select .el-input__inner) {
-  background-color: #f5f7fa;
-  border-color: #e4e7ed;
   color: #c0c4cc;
   cursor: not-allowed;
+  background-color: #f5f7fa;
+  border-color: #e4e7ed;
 }
 
 :deep(.el-form-item.is-disabled .el-date-editor .el-input__inner) {
-  background-color: #f5f7fa;
-  border-color: #e4e7ed;
   color: #c0c4cc;
   cursor: not-allowed;
+  background-color: #f5f7fa;
+  border-color: #e4e7ed;
 }
 
 /* 只读模式下表单字段的样式 */
@@ -426,16 +483,16 @@ onMounted(async () => {
 :deep(.el-form-item.is-disabled .el-textarea__inner),
 :deep(.el-form-item.is-disabled .el-select .el-input__inner),
 :deep(.el-form-item.is-disabled .el-date-editor .el-input__inner) {
-  background-color: #f5f7fa;
-  border-color: #e4e7ed;
   color: #606266; /* 保持文字可读性 */
   cursor: not-allowed;
+  background-color: #f5f7fa;
+  border-color: #e4e7ed;
 }
 
 /* 详情模式下的特殊样式 */
 .detail-mode .cell-value {
-  color: #303133;
   font-weight: normal;
+  color: #303133;
 }
 
 /* 审批意见文本域样式 */
@@ -456,14 +513,14 @@ onMounted(async () => {
 
 :deep(.el-transfer-panel__item) {
   display: flex !important;
-  align-items: center !important;
   height: 32px !important;
-  line-height: 32px !important;
   padding: 0 8px !important;
   margin: 0 !important;
-  white-space: nowrap;
   overflow: hidden;
+  line-height: 32px !important;
   text-overflow: ellipsis;
+  white-space: nowrap;
+  align-items: center !important;
 }
 
 :deep(.el-transfer-panel__list) {
@@ -472,8 +529,180 @@ onMounted(async () => {
 
 /* 只读模式下的设备显示样式 */
 :deep(.is-disabled) .device-display-container {
-  background-color: #f5f7fa;
   color: #606266;
   cursor: not-allowed;
+  background-color: #f5f7fa;
+}
+
+.maintain-record-page {
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  padding: 16px;
+  background: #f5f7fa;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  margin-bottom: 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
+}
+
+.page-header__main {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.page-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 42px;
+  height: 42px;
+  margin-right: 12px;
+  font-size: 20px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border: 1px solid var(--el-color-primary-light-7);
+  border-radius: 8px;
+}
+
+.page-title {
+  max-width: min(720px, 65vw);
+  margin: 0;
+  overflow: hidden;
+  font-size: 18px;
+  font-weight: 600;
+  line-height: 26px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.page-subtitle {
+  margin: 2px 0 0;
+  font-size: 13px;
+  line-height: 20px;
+  color: var(--el-text-color-secondary);
+}
+
+.page-section {
+  margin-bottom: 16px;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  min-height: 54px;
+  padding: 0 20px;
+  background: var(--el-fill-color-extra-light);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  flex: 0 0 auto;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.section-title span {
+  width: 4px;
+  height: 18px;
+  margin-right: 10px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.section-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  justify-content: flex-end;
+}
+
+.section-actions :deep(.el-button),
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.info-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 1px;
+  margin: 16px 20px;
+  overflow: hidden;
+  background: var(--el-border-color-lighter);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 4px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  min-height: 48px;
+  padding: 10px 14px;
+  background: var(--el-bg-color);
+}
+
+.info-label {
+  flex: 0 0 104px;
+  color: var(--el-text-color-secondary);
+  white-space: nowrap;
+}
+
+.info-value {
+  min-width: 0;
+  overflow: hidden;
+  font-weight: 500;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.query-form {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 20px;
+  padding: 16px 20px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.query-form :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.query-date {
+  width: 260px;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  gap: 8px;
+}
+
+.table-wrap {
+  padding: 16px 20px;
+}
+
+.records-section :deep(.pagination-container) {
+  padding: 12px 20px;
 }
 </style>

+ 430 - 328
src/views/pms/maintain/index.vue

@@ -1,339 +1,322 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm px-2"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item
-            :label="t('maintain.faultCode')"
-            prop="failureCode"
-            style="margin-left: 25px"
-          >
-            <el-input
-              v-model="queryParams.failureCode"
-              :placeholder="t('maintain.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('maintain.faultName')" label-width="70px" prop="failureName">
-            <el-input
-              v-model="queryParams.failureName"
-              :placeholder="t('maintain.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item label="执行时间" label-width="70px" prop="executeDate">
-            <el-date-picker
-              size="small"
-              v-model="queryParams.executeDate"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="daterange"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-              class="!w-220px"
-              :clearable="true"
-              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-            />
-          </el-form-item>
-          <el-form-item label="创建时间" label-width="70px" prop="createTime">
-            <el-date-picker
-              size="small"
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="daterange"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-              class="!w-220px"
-              :clearable="true"
-              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-            />
-          </el-form-item>
-          <el-form-item :label="t('maintain.status')" label-width="40px" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              :placeholder="t('maintain.status')"
-              clearable
-              class="!w-200px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAIN_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('maintain.shutDown')" v-show="ifShow" prop="ifStop">
-            <el-select
-              v-model="queryParams.ifStop"
-              :placeholder="t('maintain.shutDown')"
-              clearable
-              class="!w-200px"
-            >
-              <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="t('maintain.failureTime')"
-            v-show="ifShow"
-            prop="failureTime"
-            style="margin-left: 25px"
-          >
-            <el-date-picker
-              v-model="queryParams.failureTime"
-              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-220px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('maintain.solve')" v-show="ifShow" prop="ifDeal">
-            <el-select
-              v-model="queryParams.ifDeal"
-              :placeholder="t('maintain.solve')"
-              clearable
-              class="!w-200px"
-            >
-              <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="t('maintain.assist')" v-show="ifShow" prop="needHelp">
-            <el-select
-              v-model="queryParams.needHelp"
-              :placeholder="t('maintain.assist')"
-              clearable
-              class="!w-200px"
-            >
-              <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 v-if="!ifShow" @click="moreQuery(true)" type="warning"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('maintain.moreSearch') }}</el-button
-            >
-            <el-button v-if="ifShow" @click="moreQuery(false)" type="danger"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('maintain.closeSearch') }}</el-button
-            >
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('maintain.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('maintain.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-maintain:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('maintain.added') }}
-            </el-button>
-            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="maintain-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      v-model="queryParams.deptId"
+      :top-id="rootDeptId"
+      :dept-id="deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="maintain-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="maintain-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
+      <div class="query-row">
+        <el-form-item :label="t('maintain.faultCode')" prop="failureCode">
+          <el-input
+            v-model="queryParams.failureCode"
+            :placeholder="t('maintain.codeHolder')"
+            clearable
+            @keyup.enter="handleQuery"
+            class="query-control" />
+        </el-form-item>
+        <el-form-item :label="t('maintain.faultName')" label-width="70px" prop="failureName">
+          <el-input
+            v-model="queryParams.failureName"
+            :placeholder="t('maintain.nameHolder')"
+            clearable
+            @keyup.enter="handleQuery"
+            class="query-control" />
+        </el-form-item>
+        <el-form-item label="执行时间" label-width="70px" prop="executeDate">
+          <el-date-picker
+            v-model="queryParams.executeDate"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            class="query-control query-control--date"
+            :clearable="true"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" />
+        </el-form-item>
+        <el-form-item label="创建时间" label-width="70px" prop="createTime">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            class="query-control query-control--date"
+            :clearable="true"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" />
+        </el-form-item>
+        <el-form-item :label="t('maintain.status')" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            :placeholder="t('maintain.status')"
+            clearable
+            class="query-control query-control--small">
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAIN_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('maintain.shutDown')" v-show="ifShow" prop="ifStop">
+          <el-select
+            v-model="queryParams.ifStop"
+            :placeholder="t('maintain.shutDown')"
+            clearable
+            class="query-control query-control--small">
+            <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="t('maintain.failureTime')" v-show="ifShow" prop="failureTime">
+          <el-date-picker
+            v-model="queryParams.failureTime"
+            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="query-control query-control--date" />
+        </el-form-item>
+        <el-form-item :label="t('maintain.solve')" v-show="ifShow" prop="ifDeal">
+          <el-select
+            v-model="queryParams.ifDeal"
+            :placeholder="t('maintain.solve')"
+            clearable
+            class="query-control query-control--small">
+            <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="t('maintain.assist')" v-show="ifShow" prop="needHelp">
+          <el-select
+            v-model="queryParams.needHelp"
+            :placeholder="t('maintain.assist')"
+            clearable
+            class="query-control query-control--small">
+            <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>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list" height="calc(85vh - 180px)">
-          <zm-table-column :label="t('maintain.serial')" min-width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />
-          <zm-table-column :label="t('maintain.deviceName')" align="center" prop="deviceName" />
-          <zm-table-column
-            :label="t('maintain.status')"
-            align="center"
-            prop="status"
-            min-width="95"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_MAIN_STATUS" :value="scope.row.status" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('maintain.source')"
-            align="center"
-            prop="maintainType"
-            min-width="135"
-          />
-          <zm-table-column
-            :label="t('iotMaintain.repairType')"
-            align="center"
-            prop="type"
-            min-width="135"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_MAIN_TYPE" :value="scope.row.type" />
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('maintain.shutDown')" align="center" prop="ifStop">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.ifStop" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            label="执行时间"
-            align="center"
-            prop="executeDate"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column :label="t('iotMaintain.oaFlowNo')" align="center" prop="oaFlowNo" />
-          <zm-table-column
-            :label="t('maintain.dept')"
-            align="center"
-            prop="deptName"
-            min-width="135"
-          />
-          <zm-table-column
-            :label="t('inspect.createName')"
-            align="center"
-            prop="createName"
-            min-width="130"
-          />
-          <zm-table-column
-            label="创建时间"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180px"
-          />
-          <zm-table-column
-            :label="t('maintain.operation')"
-            align="center"
-            min-width="170"
-            fixed="right"
-            action
-          >
-            <template #default="scope">
-              <el-button link type="primary" @click="detail(scope.row.id)">
-                {{ t('maintain.view') }}
-              </el-button>
-              <el-button
-                link
-                v-if="
-                  scope.row.status === 'tx' &&
-                  (scope.row.auditStatus === 20 || scope.row.auditStatus === null)
-                "
-                type="danger"
-                @click="
-                  openForm('update', scope.row.id, scope.row.maintainPerson, scope.row.company)
-                "
-              >
-                {{ t('maintain.fill') }}
-              </el-button>
-              <el-button
-                link
-                v-if="scope.row.status === 'personnel' && userid === scope.row.maintainPerson"
-                type="danger"
-                @click="openDialog(scope.row.id)"
-              >
-                {{ t('maintain.maintainWay') }}
-              </el-button>
-              <el-button
-                v-if="scope.row.status !== 'personnel'"
-                link
-                type="warning"
-                @click="openRecord(scope.row.id)"
-              >
-                {{ t('maintain.record') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <el-dialog
-          v-model="dialogVisible"
-          title="维修方式"
-          :width="600"
-          :before-close="handleClose"
-          append-to-body
-          :close-on-click-modal="false"
-        >
-          <el-form ref="methodFormRef" :model="form" :rules="rules" label-width="70px">
-            <el-form-item label="维修方式" prop="maintainMethod">
-              <el-select v-model="form.maintainMethod" placeholder="请选择维修方式">
-                <el-option
-                  v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAINTAIN_METHOD)"
-                  :key="dict.label"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="备注说明" prop="methodDescription">
-              <el-input
-                type="textarea"
-                v-model="form.methodDescription"
-                placeholder="请输入备注说明"
-                :rows="4"
-                resize="none"
-              />
-            </el-form-item>
-          </el-form>
-          <template #footer>
-            <el-button @click="handleCancel">取消</el-button>
-            <el-button type="primary" @click="handleConfirm">确定</el-button>
+      <el-form-item class="query-actions">
+        <el-button v-if="!ifShow" type="warning" @click="moreQuery(true)">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('maintain.moreSearch') }}
+        </el-button>
+        <el-button v-else type="danger" @click="moreQuery(false)">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('maintain.closeSearch') }}
+        </el-button>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('maintain.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> {{ t('maintain.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['rq:iot-maintain:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> {{ t('maintain.added') }}
+        </el-button>
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn :label="t('maintain.serial')" width="70" align="center" fixed="left">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('iotDevice.code')" align="center" prop="deviceCode" />
+              <ZmTableColumn :label="t('maintain.deviceName')" align="center" prop="deviceName" />
+              <ZmTableColumn
+                :label="t('maintain.status')"
+                align="center"
+                prop="status"
+                min-width="95">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.PMS_MAIN_STATUS" :value="scope.row.status" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('maintain.source')"
+                align="center"
+                prop="maintainType"
+                min-width="135" />
+              <ZmTableColumn
+                :label="t('iotMaintain.repairType')"
+                align="center"
+                prop="type"
+                min-width="135">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.PMS_MAIN_TYPE" :value="scope.row.type" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('maintain.shutDown')" align="center" prop="ifStop">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.ifStop" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                label="执行时间"
+                align="center"
+                prop="executeDate"
+                :formatter="dateFormatter"
+                min-width="180px" />
+              <ZmTableColumn :label="t('iotMaintain.oaFlowNo')" align="center" prop="oaFlowNo" />
+              <ZmTableColumn
+                :label="t('maintain.dept')"
+                align="center"
+                prop="deptName"
+                min-width="135" />
+              <ZmTableColumn
+                :label="t('inspect.createName')"
+                align="center"
+                prop="createName"
+                min-width="130" />
+              <ZmTableColumn
+                label="创建时间"
+                align="center"
+                prop="createTime"
+                :formatter="dateFormatter"
+                width="180px" />
+              <ZmTableColumn
+                :label="t('maintain.operation')"
+                align="center"
+                min-width="170"
+                fixed="right"
+                action>
+                <template #default="scope">
+                  <el-button link type="primary" @click="detail(scope.row.id)">
+                    {{ t('maintain.view') }}
+                  </el-button>
+                  <el-button
+                    link
+                    v-if="
+                      scope.row.status === 'tx' &&
+                      (scope.row.auditStatus === 20 || scope.row.auditStatus === null)
+                    "
+                    type="danger"
+                    @click="
+                      openForm('update', scope.row.id, scope.row.maintainPerson, scope.row.company)
+                    ">
+                    {{ t('maintain.fill') }}
+                  </el-button>
+                  <el-button
+                    link
+                    v-if="scope.row.status === 'personnel' && userid === scope.row.maintainPerson"
+                    type="danger"
+                    @click="openDialog(scope.row.id)">
+                    {{ t('maintain.maintainWay') }}
+                  </el-button>
+                  <el-button
+                    v-if="scope.row.status !== 'personnel'"
+                    link
+                    type="warning"
+                    @click="openRecord(scope.row.id)">
+                    {{ t('maintain.record') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
           </template>
-        </el-dialog>
-        <!-- 分页 -->
-        <Pagination
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <!-- 表单弹窗:添加/修改 -->
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
+  <el-dialog
+    v-model="dialogVisible"
+    title="维修方式"
+    :width="600"
+    :before-close="handleClose"
+    append-to-body
+    :close-on-click-modal="false">
+    <el-form ref="methodFormRef" :model="form" :rules="rules" label-width="70px">
+      <el-form-item label="维修方式" prop="maintainMethod">
+        <el-select v-model="form.maintainMethod" placeholder="请选择维修方式">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PMS_MAINTAIN_METHOD)"
+            :key="dict.label"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="备注说明" prop="methodDescription">
+        <el-input
+          type="textarea"
+          v-model="form.methodDescription"
+          placeholder="请输入备注说明"
+          :rows="4"
+          resize="none" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="handleCancel">取消</el-button>
+      <el-button type="primary" @click="handleConfirm">确定</el-button>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotMaintainApi, IotMaintainVO } from '@/api/pms/maintain'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
 import { useUserStore } from '@/store/modules/user'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotMaintainVO>()
 
 /** 维修工单 列表 */
 defineOptions({ name: 'IotMaintain' })
@@ -346,7 +329,8 @@ const loading = ref(true) // 列表的加载中
 const list = ref<IotMaintainVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const dialogVisible = ref(false)
-let isLeftContentCollapsed = ref(false)
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -460,6 +444,7 @@ const getList = async () => {
 /** 处理部门被点击 */
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 /** 搜索按钮操作 */
@@ -470,10 +455,20 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
@@ -510,11 +505,118 @@ onMounted(() => {
 })
 </script>
 
-<style lang="scss" scoped>
-::v-deep .el-table__header-wrapper {
-  position: sticky !important;
+<style scoped>
+.maintain-query {
+  display: block;
+}
+
+.query-row {
+  display: flex;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 24px;
+  min-width: 0;
+}
+
+.query-row :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+.query-actions {
   width: 100%;
-  top: 0px;
-  z-index: 2000;
+  margin-top: 12px;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 180px;
+}
+
+.query-control--small {
+  width: 140px;
+}
+
+.query-control--date {
+  width: 220px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width <= 1500px) {
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 168px;
+  }
+
+  .query-control--small {
+    width: 132px;
+  }
+
+  .query-control--date {
+    width: 210px;
+  }
+}
+
+@media (width <= 1200px) {
+  .maintain-page {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  :deep(.maintain-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+}
+
+@media (width <= 768px) {
+  .maintain-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--small,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+    margin-left: 0;
+  }
 }
 </style>

+ 43 - 22
src/views/pms/maintain/material/MaintainMaterialDrawer.vue

@@ -7,33 +7,42 @@
     :show-close="false"
     direction="rtl"
     :size="computedSize"
-    :before-close="handleClose"
-  >
+    :before-close="handleClose">
     <el-form
       ref="drawerRef"
       :model="drawerFormData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
-    >
+      v-loading="formLoading">
       <el-form-item :label="t('stock.MaterialMaster')" prop="code1" style="margin-bottom: 30px">
         <el-select
           v-model="drawerFormData.code1"
           :model-value="materialLabel"
           :placeholder="t('stock.MaterialMaster')"
-          @click="openForm"
-        />
+          @click="openForm" />
       </el-form-item>
-      <el-form-item :label="t('workOrderMaterial.materialCode')" prop="code1" style="margin-bottom: 30px">
+      <el-form-item
+        :label="t('workOrderMaterial.materialCode')"
+        prop="code1"
+        style="margin-bottom: 30px">
         <el-input v-model="drawerFormData.code1" disabled />
       </el-form-item>
-      <el-form-item :label="t('workOrderMaterial.materialName')" prop="name1" style="margin-bottom: 30px">
+      <el-form-item
+        :label="t('workOrderMaterial.materialName')"
+        prop="name1"
+        style="margin-bottom: 30px">
         <el-input v-model="drawerFormData.name1" disabled />
       </el-form-item>
       <el-form-item :label="t('workOrderMaterial.unit')" prop="unit1" style="margin-bottom: 30px">
-        <el-input v-model="drawerFormData.unit1" disabled :placeholder="t('workOrderMaterial.unit')" />
+        <el-input
+          v-model="drawerFormData.unit1"
+          disabled
+          :placeholder="t('workOrderMaterial.unit')" />
       </el-form-item>
-      <el-form-item :label="t('workOrderMaterial.ConsumptionQuantity')" prop="depleteCount1" style="margin-bottom: 30px">
+      <el-form-item
+        :label="t('workOrderMaterial.ConsumptionQuantity')"
+        prop="depleteCount1"
+        style="margin-bottom: 30px">
         <el-input-number
           v-model="drawerFormData.depleteCount1"
           :min="0"
@@ -41,25 +50,31 @@
           :step="0.1"
           style="width: 50%"
           class="w-100%"
-          controls-position="right"
-        />
+          controls-position="right" />
       </el-form-item>
-      <el-form-item :label="t('workOrderMaterial.unitPrice')" prop="price1" style="margin-bottom: 30px">
+      <el-form-item
+        :label="t('workOrderMaterial.unitPrice')"
+        prop="price1"
+        style="margin-bottom: 30px">
         <el-input-number
           v-model="drawerFormData.price1"
           :min="0"
           :precision="2"
           :step="0.1"
           style="width: 50%"
-          controls-position="right"
-        />
+          controls-position="right" />
       </el-form-item>
       <el-form-item :label="t('deviceForm.remark')" prop="remark1" style="margin-bottom: 30px">
-        <el-input v-model="drawerFormData.remark1" type="textarea" :placeholder="t('deviceForm.remarkHolder')" />
+        <el-input
+          v-model="drawerFormData.remark1"
+          type="textarea"
+          :placeholder="t('deviceForm.remarkHolder')" />
       </el-form-item>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('common.ok') }}</el-button>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{
+        t('common.ok')
+      }}</el-button>
       <el-button @click="handleClose">{{ t('common.cancel') }}</el-button>
     </template>
   </el-drawer>
@@ -73,6 +88,7 @@ import { CommonStatusEnum } from '@/utils/constants'
 import MaterialSelect from './MaterialSelect.vue'
 const { t } = useI18n() // 国际化
 const drawerVisible = ref<boolean>(false)
+const formLoading = ref(false)
 const emit = defineEmits(['update:modelValue', 'add'])
 const materialLabel = ref('') // 表单的类型:create - 新增;update - 修改
 defineOptions({
@@ -125,7 +141,7 @@ const formRules = reactive({
   code1: [{ required: true, message: '物料编码不能为空', trigger: 'blur' }],
   name1: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
   depleteCount1: [{ required: true, message: '消耗数量不能为空', trigger: 'blur' }],
-  price1:[{ required: true, message: '单价不能为空', trigger: 'blur' }],
+  price1: [{ required: true, message: '单价不能为空', trigger: 'blur' }]
 })
 const materialFormRef = ref()
 const openForm = () => {
@@ -163,10 +179,15 @@ const handleClose = () => {
 }
 const drawerRef = ref()
 const submitForm = async () => {
-  await drawerRef.value.validate()
-  drawerFormData.value.sourceType = '手动添加'
-  emit('add', drawerFormData.value)
-  emit('update:modelValue', false)
+  formLoading.value = true
+  try {
+    await drawerRef.value.validate()
+    drawerFormData.value.sourceType = '手动添加'
+    emit('add', drawerFormData.value)
+    emit('update:modelValue', false)
+  } finally {
+    formLoading.value = false
+  }
 }
 
 defineExpose({ openDrawer, closeDrawer }) // 暴露方法给父组件

+ 8 - 1
src/views/pms/qhse/MeasureCertDrawer.vue

@@ -85,6 +85,13 @@
                   }}</span>
                 </template>
               </zm-table-column>
+
+              <zm-table-column label="检测周期" align="center" width="140">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.QHSE_DETECT_CYCLE" :value="scope.row.detectCycle" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="精度" align="center" prop="precisions" />
               <zm-table-column label="校准金额" align="center" prop="detectAmount" />
               <zm-table-column label="附件" fixed="right" align="center" prop="file" width="90">
                 <template #default="scope">
@@ -144,7 +151,7 @@ import { IotMeasureDetectApi } from '@/api/pms/qhse/index'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { formatDate } from '@/utils/formatTime'
 import IotMeasureDetectForm from './iotmeasuredetect/IotMeasureDetectForm.vue'
-
+import { DICT_TYPE } from '@/utils/dict'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 
 defineOptions({ name: 'MeasureCertDrawer' })

+ 36 - 14
src/views/pms/qhse/certificate.vue

@@ -187,12 +187,19 @@
                   <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
                 </template>
               </zm-table-column>
+
               <zm-table-column
                 label="证书名称"
                 align="center"
                 prop="certName"
                 show-overflow-tooltip />
 
+              <zm-table-column label="培训周期" align="center" width="100">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.QHSE_TRAIN_CYCLE" :value="scope.row.trainCycle" />
+                </template>
+              </zm-table-column>
+
               <zm-table-column label="所属人" align="center" prop="userName" />
               <zm-table-column label="所在部门" align="center" prop="deptName" />
 
@@ -305,6 +312,16 @@
         <el-input v-model="formData.certName" placeholder="请输入证书名称" />
       </el-form-item>
 
+      <el-form-item label="培训周期" prop="trainCycle">
+        <el-select v-model="formData.trainCycle" placeholder="培训周期" clearable>
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.QHSE_TRAIN_CYCLE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+
       <el-form-item label="所在部门" prop="deptId">
         <el-tree-select
           clearable
@@ -382,25 +399,25 @@
     </template>
   </Dialog>
 
-  <Dialog v-model="dialogFileView" title="证书附件">
+  <Dialog v-model="dialogFileView" title="附件">
     <div
       v-for="(file, index) in fileList"
       :key="index"
-      class="flex items-center justify-between mt-5">
-      <span class="file-name-text text-ellipsis!">{{ extractFileName(file) }}</span>
-      <div>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
-          <Icon icon="ep:view" class="mr-2px" />查看</el-button
-        >
+          <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
-        >
+          <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>
+        <el-button type="primary" @click="dialogFileView = false">确认</el-button>
       </div>
     </template>
   </Dialog>
@@ -408,7 +425,6 @@
 
 <script setup lang="ts">
 import { IotMeasureCertApi } from '@/api/pms/qhse/index'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import { ElMessageBox, ElMessage } from 'element-plus'
@@ -430,7 +446,6 @@ const loading = ref(true) // 列表的加载中
 const formLoading = ref(false) // 表单加载中
 const submitLoading = ref(false) // 提交按钮加载中
 const exportLoading = ref(false) // 导出按钮加载中
-const isLeftContentCollapsed = ref(false)
 
 const { t } = useI18n()
 
@@ -454,7 +469,6 @@ const isEdit = ref(false)
 
 // 图片查看对话框
 const dialogFileView = ref(false)
-const imagePreviewUrl = ref('')
 
 // 表单相关
 const formRef = ref()
@@ -470,7 +484,8 @@ const formData = ref({
   noticeBefore: '', // 到期前提醒
   certPic: '', // 证书图片上传
   remark: '', // 备注
-  deptId: '' // 部门id
+  deptId: '', // 部门id
+  trainCycle: ''
 })
 
 // 获取证书类型文本
@@ -523,7 +538,8 @@ const formRules = {
   certOrg: [{ required: true, message: '颁发机构不能为空', trigger: 'blur' }],
   certIssue: [{ required: true, message: '颁发时间不能为空', trigger: 'blur' }],
   certExpire: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
-  certPic: [{ required: true, message: '证书图片不能为空', trigger: 'blur' }]
+  certPic: [{ required: true, message: '证书图片不能为空', trigger: 'blur' }],
+  trainCycle: [{ required: true, message: '培训周期不能为空', trigger: 'blur' }]
 }
 
 /** 查询列表 */
@@ -862,6 +878,12 @@ onMounted(async () => {
 </script>
 
 <style scoped>
+.file-name-text {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
 ::deep(.el-tree--highlight-current) {
   height: 200px !important;
 }

+ 15 - 10
src/views/pms/qhse/credential/index.vue

@@ -328,25 +328,25 @@
     </template>
   </Dialog>
 
-  <Dialog v-model="dialogFileView" title="证书附件">
+  <Dialog v-model="dialogFileView" title="附件">
     <div
       v-for="(file, index) in fileList"
       :key="index"
-      class="flex items-center justify-between mt-5">
-      <span class="file-name-text text-ellipsis!">{{ extractFileName(file) }}</span>
-      <div>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
-          <Icon icon="ep:view" class="mr-2px" />查看</el-button
-        >
+          <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
-        >
+          <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>
+        <el-button type="primary" @click="dialogFileView = false">确认</el-button>
       </div>
     </template>
   </Dialog>
@@ -782,7 +782,12 @@ onMounted(async () => {
 ::deep(.el-transfer-panel__body) {
   height: 700px !important;
 }
-
+.file-name-text {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
 .stats-cards {
   display: grid;
   grid-template-columns: repeat(3, minmax(0, 1fr));

+ 177 - 0
src/views/pms/qhse/data/IotInfoFormTree.vue

@@ -0,0 +1,177 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" style="width: 50vw">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading">
+      <el-form-item :label="t('fileInfo.fileDirectory')" prop="classId">
+        <el-input v-model="displayNodeName" disabled placeholder="请选择文件目录" />
+      </el-form-item>
+      <el-form-item :label="t('fileInfo.appendix')" prop="filePath">
+        <file-upload
+          style="height: 300px"
+          @uploadSuccess="handleUploadSuccess"
+          @uploadError="handleUploadError"
+          @uploadComplete="handleUploadComplete"
+          :deviceId="props.classId" />
+      </el-form-item>
+      <el-form-item :label="t('fileInfo.fileType')" prop="fileType">
+        <el-select
+          v-model="formData.fileType"
+          placeholder="请选择"
+          :disabled="formType === 'detail'">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
+            :key="dict.label"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('fileInfo.remark')" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          :disabled="formType === 'detail'"
+          type="textarea"
+          :placeholder="t('deviceForm.remarkHolder')" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button
+        v-if="formType !== 'detail'"
+        type="primary"
+        :disabled="formLoading"
+        @click="submitForm">
+        {{ t('common.ok') }}
+      </el-button>
+      <el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DataLibraryApi } from '@/api/pms/qhse/index'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import FileUpload from '@/components/UploadFile/src/FileUpload.vue'
+
+defineOptions({ name: 'IotInfoFormTree' })
+
+const { t } = useI18n()
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const displayNodeName = ref('')
+const formRef = ref()
+const formRules = reactive({})
+
+const formData = ref({
+  id: undefined,
+  deviceId: undefined,
+  classId: undefined,
+  deptId: undefined,
+  filename: undefined,
+  fileType: undefined,
+  filePath: undefined,
+  remark: undefined,
+  fileList: [],
+  folderTree: [],
+  folderJson: undefined
+})
+
+const props = defineProps<{
+  deviceId: string
+  classId: string
+  nodeId: string
+  deptId: string
+  nodeName: string
+}>()
+
+const emit = defineEmits(['success'])
+
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await DataLibraryApi.getIotInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    formData.value.fileType = 'file'
+  }
+
+  displayNodeName.value = props.nodeName
+  formData.value.classId = props.classId
+  formData.value.deptId = props.deptId
+}
+
+defineExpose({ open })
+
+const submitForm = async () => {
+  await formRef.value.validate()
+  formLoading.value = true
+
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      data.deviceId = props.deviceId
+      data.classId = props.classId
+      data.deptId = props.deptId
+      data.folderJson = JSON.stringify(data.folderTree)
+      await DataLibraryApi.createIotInfo(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DataLibraryApi.updateIotInfo(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceId: undefined,
+    classId: undefined,
+    deptId: undefined,
+    filename: undefined,
+    fileType: undefined,
+    filePath: undefined,
+    remark: undefined,
+    fileList: [],
+    folderTree: [],
+    folderJson: undefined
+  }
+  displayNodeName.value = ''
+  formRef.value?.resetFields()
+}
+
+const handleUploadSuccess = (data) => {
+  if (!Array.isArray(formData.value.folderTree)) {
+    formData.value.folderTree = []
+  }
+  formData.value.folderTree.push(data.response.data)
+  message.success(`"${data.name}" 上传成功`)
+}
+
+const handleUploadError = (error) => {
+  console.error('上传失败', error)
+  message.error(`"${error.name}" 上传失败: ${error.error.message}`)
+}
+
+const handleUploadComplete = () => {
+  message.info('所有上传任务已处理完成')
+}
+</script>

+ 0 - 129
src/views/pms/qhse/data/IotTreeForm.vue

@@ -1,129 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="原id" prop="originId">
-        <el-input v-model="formData.originId" placeholder="请输入原id" />
-      </el-form-item>
-      <el-form-item label="父分类id" prop="parentId">
-        <el-input v-model="formData.parentId" placeholder="请输入父分类id" />
-      </el-form-item>
-      <el-form-item label="分类名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入分类名称" />
-      </el-form-item>
-      <el-form-item label="类型" prop="type">
-        <el-select v-model="formData.type" placeholder="请选择类型">
-          <el-option label="请选择字典生成" value="" />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="分类排序" prop="sort">
-        <el-input v-model="formData.sort" placeholder="请输入分类排序" />
-      </el-form-item>
-      <el-form-item label="开启状态" prop="status">
-        <el-radio-group v-model="formData.status">
-          <el-radio value="1">请选择字典生成</el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import { IotTreeApi, IotTreeVO } from '@/api/system/tree'
-
-/** pms树 表单 */
-defineOptions({ name: 'IotTreeForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  originId: undefined,
-  parentId: undefined,
-  name: undefined,
-  type: undefined,
-  sort: undefined,
-  status: undefined,
-  remark: undefined,
-})
-const formRules = reactive({
-  originId: [{ required: true, message: '原id不能为空', trigger: 'blur' }],
-  parentId: [{ required: true, message: '父分类id不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
-  type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await IotTreeApi.getIotTree(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as IotTreeVO
-    if (formType.value === 'create') {
-      await IotTreeApi.createIotTree(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await IotTreeApi.updateIotTree(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    originId: undefined,
-    parentId: undefined,
-    name: undefined,
-    type: undefined,
-    sort: undefined,
-    status: undefined,
-    remark: undefined,
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 413 - 289
src/views/pms/qhse/data/PmsTree.vue

@@ -1,118 +1,28 @@
-<template>
-  <div class="head-container">
-    <el-input v-model="deptName" class="mb-15px" clearable placeholder="请输入名称">
-      <template #prefix>
-        <Icon icon="ep:search" />
-      </template>
-    </el-input>
-  </div>
-  <div ref="treeContainer" class="tree-container">
-    <el-tree
-      ref="treeRef"
-      :data="treeList"
-      :expand-on-click-node="false"
-      :filter-node-method="filterNode"
-      :props="defaultProps"
-      :default-expanded-keys="expandedKeys"
-      highlight-current
-      node-key="id"
-      @node-click="handleNodeClick"
-      @node-contextmenu="handleRightClick"
-      style="height: 52em">
-      <template #default="{ node }">
-        <div
-          style="display: flex; justify-content: space-between; align-items: center; width: 100%">
-          <div>
-            <Icon
-              style="vertical-align: middle; fill: currentColor; color: orange"
-              v-if="node.data.type === 'dept'"
-              icon="fa:folder-open" />
-            <Icon
-              style="vertical-align: middle; fill: currentColor; color: dodgerblue"
-              v-if="node.data.type === 'device'"
-              icon="fa:tasks" />
-            <Icon
-              icon="fa:folder-open"
-              v-if="node.data.type === 'file'"
-              style="vertical-align: middle; color: orange; fill: currentColor" />
-            <span style="vertical-align: middle; margin-left: 3px">{{ node.data.name }}</span>
-          </div>
-        </div>
-      </template>
-    </el-tree>
-  </div>
-  <div
-    v-show="deviceVisible"
-    ref="contextMenuRef"
-    class="custom-menu"
-    :style="{ left: menuX + 'px', top: menuY + 'px' }">
-    <ul>
-      <li style="border-bottom: 1px solid #ccc" @click="handleDeviceClick('add')">设备详情</li>
-      <li style="border-bottom: 0px solid #ccc" @click="handleMenuClick('add')">新建目录</li>
-    </ul>
-  </div>
-  <div
-    v-show="menuVisible"
-    ref="contextMenuRef"
-    class="custom-menu"
-    :style="{ left: menuX + 'px', top: menuY + 'px' }">
-    <ul>
-      <li style="border-bottom: 1px solid #ccc" @click="handleMenuClick('add')">新建目录</li>
-      <li style="border-bottom: 1px solid #ccc" @click="handleMenuClick('edit')">编辑</li>
-      <li @click="handleMenuClick('delete')">删除目录</li>
-    </ul>
-  </div>
-
-  <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px">
-      <el-form-item label="目录名称" prop="name" label-width="110px">
-        <el-input v-model="formData.name" placeholder="请输入目录名称" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort" label-width="110px">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
 <script lang="ts" setup>
 import { ElTree } from 'element-plus'
+import { CaretLeft, CaretRight, Search } from '@element-plus/icons-vue'
+import type { IotTreeVO } from '@/api/system/tree'
+import { DataLibraryApi } from '@/api/pms/qhse/index'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { CommonStatusEnum } from '@/utils/constants'
-import { IotTreeApi } from '@/api/system/tree'
-import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useI18n } from 'vue-i18n'
-import { useRouter } from 'vue-router'
-
-// 类型定义
-interface Tree {
-  id: number | string
-  name: string
-  type: string
-  children?: Tree[]
-  [key: string]: any
-}
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
+
+defineOptions({ name: 'IotTree' })
+
+const formLoading = ref(false)
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formRef = ref() // 搜索的表单
-const firstLevelKeys = ref<(number | string)[]>([])
-const expandedKeys = ref<(number | string)[]>([]) // 用于展开节点的路径
-
-// 表单相关
-const formLoading = ref(false)
+const message = useMessage()
+const nodeInfo = ref({})
+const menuVisible = ref(false)
+const menuX = ref(0)
+const menuY = ref(0)
+const contextMenuRef = ref(null) // 弹窗DOM引用
+const { t } = useI18n()
+let selectedNode = null
+const parentId = ref()
 const formType = ref<'create' | 'update'>('create')
+
 const formData = ref({
   id: undefined,
   title: '',
@@ -128,8 +38,26 @@ const formRules = ref({
   name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
   sort: [{ required: true, message: '请输入排序', trigger: 'blur' }]
 })
+const handleRightClick = (event, data, node) => {
+  nodeInfo.value = { ...data }
+  event.preventDefault()
+  const nodeContent = (event.target as HTMLElement)?.closest(
+    '.el-tree-node__content'
+  ) as HTMLElement | null
+  if (nodeContent) {
+    const rect = nodeContent.getBoundingClientRect()
+    menuX.value = rect.right + 8
+    menuY.value = rect.top
+  } else {
+    menuX.value = event.clientX
+    menuY.value = event.clientY
+  }
+  selectedNode = data
+  parentId.value = data.id
+
+  menuVisible.value = true
+}
 
-/** 重置表单 */
 const resetForm = () => {
   formData.value = {
     id: undefined,
@@ -145,113 +73,6 @@ const resetForm = () => {
   formRef.value?.resetFields()
 }
 
-defineOptions({ name: 'IotTree' })
-
-// 接收外部参数
-const props = defineProps({
-  deviceId: { type: Number, required: true },
-  currentId: { type: [Number, String], default: null } // 新增:接收需要定位的节点ID
-})
-
-const deptName = ref('')
-const nodeInfo = ref({})
-const treeList = ref<Tree[]>([]) // 树形结构
-const treeRef = ref<InstanceType<typeof ElTree>>()
-const menuVisible = ref(false)
-const deviceVisible = ref(false)
-const menuX = ref(0)
-const menuY = ref(0)
-const parentId = ref()
-
-const { push } = useRouter() // 路由跳转
-
-// 动态高度计算
-const treeContainer = ref(null)
-const setHeight = () => {
-  if (!treeContainer.value) return
-  const windowHeight = window.innerHeight
-  const containerTop = treeContainer.value.offsetTop
-  treeContainer.value.style.height = `${windowHeight * 0.78}px` // 60px 底部预留
-}
-
-// 新增:查找节点路径的递归方法
-const findNodePath = (
-  nodes: Tree[],
-  targetId: number | string,
-  path: (number | string)[] = []
-): (number | string)[] | null => {
-  for (const node of nodes) {
-    path.push(node.id)
-    if (node.id === targetId) {
-      return [...path]
-    }
-    if (node.children && node.children.length) {
-      const result = findNodePath(node.children, targetId, path)
-      if (result) {
-        return result
-      }
-    }
-    path.pop()
-  }
-  return null
-}
-
-// 新增:定位节点并高亮
-const locateNode = (targetId: number | string) => {
-  if (!targetId || !treeList.value.length) return
-
-  const pathIds = findNodePath(treeList.value, targetId)
-  if (pathIds) {
-    // 展开所有父节点
-    expandedKeys.value = pathIds.slice(0, -1)
-
-    // 等待DOM更新后设置当前节点
-    nextTick(() => {
-      if (treeRef.value) {
-        treeRef.value.setCurrentKey(targetId)
-
-        // 滚动到节点位置
-        const node = treeRef.value.getNode(targetId)
-        if (node && node.$el) {
-          node.$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
-        }
-      }
-    })
-  }
-}
-
-onMounted(async () => {
-  await getTreeInfo()
-  setHeight()
-  window.addEventListener('resize', setHeight)
-})
-
-onUnmounted(() => {
-  window.removeEventListener('resize', setHeight)
-})
-
-const handleRightClick = (event, node, data) => {
-  nodeInfo.value = node
-  event.preventDefault()
-  menuX.value = event.clientX
-  menuY.value = event.clientY
-  selectedNode = data
-  debugger
-  parentId.value = data.data.id
-  if (nodeInfo.value.type === 'device') {
-    deviceVisible.value = true
-  } else if (nodeInfo.value.type === 'file') {
-    menuVisible.value = true
-  }
-}
-
-const handleDeviceClick = async () => {
-  const id = nodeInfo.value.originId
-  push({ name: 'DeviceDetailInfo', params: { id } })
-  deviceVisible.value = false
-  menuVisible.value = false
-}
-
 const handleMenuClick = async (action) => {
   switch (action) {
     case 'add':
@@ -266,72 +87,52 @@ const handleMenuClick = async (action) => {
       dialogTitle.value = '编辑目录'
       formType.value = 'update'
       formData.value = { ...nodeInfo.value }
-      debugger
+
       break
     case 'delete':
       // 删除的二次确认
       await message.delConfirm()
       // 假设存在删除接口
-      await IotTreeApi.deleteIotTree(nodeInfo.value.id)
+
+      await DataLibraryApi.deleteFileTree(nodeInfo.value.id)
+
       message.success(t('common.delSuccess'))
       // 刷新列表
       await getTreeInfo()
       break
   }
-  deviceVisible.value = false
-  menuVisible.value = false
-}
-
-/** 获得部门树 */
-const getTreeInfo = async () => {
-  const res = await IotTreeApi.getSimpleTreeList()
-  treeList.value = []
-  treeList.value.push(...handleTree(res))
-
-  // 处理展开逻辑:有currentId则展开对应路径,否则展开一级节点
-  if (props.currentId) {
-    locateNode(props.currentId)
-  } else {
-    firstLevelKeys.value = treeList.value.map((node) => node.id)
-    expandedKeys.value = [...firstLevelKeys.value]
-  }
-  emits('success', treeList.value[0]?.id)
-}
-
-/** 基于名字过滤 */
-const filterNode = (name: string, data: Tree) => {
-  if (!name) return true
-  return data.name.includes(name)
-}
 
-/** 处理节点被点击 */
-const handleNodeClick = async (row: { [key: string]: any }) => {
-  deviceVisible.value = false
   menuVisible.value = false
-  emits('node-click', row)
+  // nextTick(() => {
+  //   treeRef.value?.setCurrentKey(targetId)
+  //   treeRef.value?.getNode(targetId)?.$el?.scrollIntoView({
+  //     behavior: 'smooth',
+  //     block: 'nearest'
+  //   })
+  // })
 }
 
-/** 提交表单 */
 const submitForm = async () => {
   // 表单验证逻辑
   formLoading.value = true
   try {
-    formData.value.deviceId = props.deviceId
-    debugger
-    // formData.value.parentId = parentId.value
-    if (formData.value.parentId === undefined || formData.value.parentId === null) {
-      formData.value.parentId = props.currentId
-    }
+    const targetParentId = formType.value === 'create' ? parentId.value : formData.value.parentId
     formData.value.type = 'file'
     if (formType.value === 'create') {
-      debugger
-      await IotTreeApi.createIotTree(formData.value)
+      formData.value.parentId = parentId.value
+
+      await DataLibraryApi.createFileTree(formData.value)
     } else {
-      await IotTreeApi.updateIotTree(formData.value)
+      await DataLibraryApi.updateFileTree(formData.value)
     }
-    message.success(t(formType.value === 'create' ? 'common.addSuccess' : 'common.updateSuccess'))
+    message.success(t(formType.value === 'create' ? '添加成功' : '修改成功'))
     dialogVisible.value = false
+    menuVisible.value = false
     await getTreeInfo()
+    if (targetParentId) {
+      expandedKeys.value = [...new Set([...expandedKeys.value, targetParentId])]
+      locateNode(targetParentId)
+    }
   } catch (error) {
     console.error(error)
   } finally {
@@ -339,66 +140,389 @@ const submitForm = async () => {
   }
 }
 
-const emits = defineEmits(['node-click', 'success'])
+interface TreeNode extends IotTreeVO {
+  children?: TreeNode[]
+}
+
+type TreeKey = number | string
 
-/** 监听搜索框变化 */
-watch(deptName, (val) => {
-  treeRef.value!.filter(val)
+const props = withDefaults(
+  defineProps<{
+    deviceId: number
+    currentId?: TreeKey | null
+  }>(),
+  {
+    currentId: null
+  }
+)
+
+const emits = defineEmits<{
+  (event: 'node-click', value: TreeNode): void
+  (event: 'success', value?: number): void
+}>()
+
+const isCollapsed = ref(false)
+const treeName = ref('')
+const treeList = ref<TreeNode[]>([])
+const treeRef = ref<InstanceType<typeof ElTree>>()
+const expandedKeys = ref<Array<number | string>>([])
+
+const getNodeIcon = (type: string) => {
+  if (type === 'device') return 'fa:tasks'
+  return 'fa:folder-open'
+}
+
+const getNodeIconClass = (type: string) => {
+  return type === 'device' ? 'node-icon node-icon--device' : 'node-icon node-icon--folder'
+}
+
+const filterDeviceNodes = (nodes: TreeNode[]): TreeNode[] => {
+  return nodes.reduce<TreeNode[]>((result, node) => {
+    if (node.type === 'device') {
+      return result
+    }
+
+    const nextNode: TreeNode = { ...node }
+    if (node.children?.length) {
+      nextNode.children = filterDeviceNodes(node.children)
+    }
+
+    result.push(nextNode)
+    return result
+  }, [])
+}
+
+const findNodePath = (
+  nodes: TreeNode[],
+  targetId: number | string,
+  path: Array<number | string> = []
+): Array<number | string> | null => {
+  for (const node of nodes) {
+    const nextPath = [...path, node.id]
+
+    if (node.id === targetId) {
+      return nextPath
+    }
+
+    if (node.children?.length) {
+      const result = findNodePath(node.children, targetId, nextPath)
+      if (result) return result
+    }
+  }
+
+  return null
+}
+
+const locateNode = (targetId: number | string) => {
+  if (!targetId || !treeList.value.length) return
+
+  const pathIds = findNodePath(treeList.value, targetId)
+  if (!pathIds) return
+
+  expandedKeys.value = [...new Set([...expandedKeys.value, ...pathIds.slice(0, -1)])]
+
+  nextTick(() => {
+    treeRef.value?.setCurrentKey(targetId)
+    treeRef.value?.getNode(targetId)?.$el?.scrollIntoView({
+      behavior: 'smooth',
+      block: 'nearest'
+    })
+  })
+}
+
+const getTreeInfo = async () => {
+  const res = await DataLibraryApi.getFileTreeList()
+  treeList.value = filterDeviceNodes(handleTree(res) as TreeNode[])
+  expandedKeys.value = treeList.value.map((node) => node.id)
+
+  if (props.currentId) {
+    locateNode(props.currentId)
+  }
+
+  emits('success', treeList.value[0]?.id)
+}
+
+const filterNode = (name: string, data: TreeNode) => {
+  return !name || data.name.includes(name)
+}
+
+const handleNodeClick = (row: TreeNode) => {
+  emits('node-click', row)
+}
+
+watch(treeName, (val) => {
+  treeRef.value?.filter(val)
 })
 
-/** 监听currentId变化,重新定位 */
 watch(
   () => props.currentId,
   (newVal) => {
-    if (newVal && treeList.value.length) {
+    if (newVal) {
       locateNode(newVal)
     }
-  },
-  { immediate: true }
+  }
 )
+
+const handleClickOutside = (event) => {
+  if (menuVisible.value && contextMenuRef.value && !contextMenuRef.value.contains(event.target)) {
+    menuVisible.value = false
+  }
+}
+
+watch(menuVisible, (visible) => {
+  if (visible) {
+    document.addEventListener('click', handleClickOutside)
+  } else {
+    document.removeEventListener('click', handleClickOutside)
+  }
+})
+
+onMounted(getTreeInfo)
+onBeforeUnmount(() => {
+  document.removeEventListener('click', handleClickOutside)
+})
 </script>
 
+<template>
+  <div
+    class="pms-tree-aside relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg transition-all duration-300 ease-in-out overflow-visible"
+    :class="[isCollapsed ? 'is-collapsed' : 'p-4']">
+    <div v-show="!isCollapsed" class="h-full flex flex-col gap-4 overflow-hidden w-full">
+      <div class="shrink-0">
+        <el-input
+          v-model="treeName"
+          clearable
+          placeholder="请输入名称"
+          size="default"
+          :prefix-icon="Search" />
+      </div>
+
+      <div class="tree-body">
+        <el-auto-resizer class="absolute">
+          <template #default="{ height }">
+            <el-scrollbar :style="{ height: `${height}px` }">
+              <el-tree
+                ref="treeRef"
+                :data="treeList"
+                :default-expanded-keys="expandedKeys"
+                :expand-on-click-node="false"
+                :filter-node-method="filterNode"
+                :props="defaultProps"
+                highlight-current
+                node-key="id"
+                @node-contextmenu="handleRightClick"
+                @node-click="handleNodeClick">
+                <template #default="{ node }">
+                  <div class="tree-node">
+                    <Icon
+                      :class="getNodeIconClass(node.data.type)"
+                      :icon="getNodeIcon(node.data.type)" />
+                    <span class="tree-node__label">{{ node.data.name }}</span>
+                  </div>
+                </template>
+              </el-tree>
+
+              <div
+                v-show="menuVisible"
+                ref="contextMenuRef"
+                class="custom-menu"
+                :style="{ left: menuX + 'px', top: menuY + 'px' }">
+                <ul>
+                  <li @click="handleMenuClick('add')" class="flex items-center gap-1">
+                    <Icon icon="ep:plus" /> 新建目录</li
+                  >
+
+                  <li @click="handleMenuClick('edit')" class="flex items-center gap-1">
+                    <Icon icon="ep:edit" />编辑目录</li
+                  >
+
+                  <li @click="handleMenuClick('delete')" class="flex items-center gap-1"
+                    ><Icon icon="ep:delete" />删除目录</li
+                  >
+                </ul>
+              </div>
+            </el-scrollbar>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+
+    <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
+      <el-form
+        ref="formRef"
+        v-loading="formLoading"
+        :model="formData"
+        :rules="formRules"
+        label-width="80px">
+        <div class="flex flex-col gap-4">
+          <el-form-item label="目录名称" prop="name" label-width="110px">
+            <el-input v-model="formData.name" placeholder="请输入目录名称" />
+          </el-form-item>
+          <el-form-item label="显示排序" prop="sort" label-width="110px">
+            <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+          </el-form-item>
+        </div>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </template>
+    </Dialog>
+
+    <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
+      <el-icon size="12">
+        <CaretLeft v-if="!isCollapsed" />
+        <CaretRight v-else />
+      </el-icon>
+    </div>
+  </div>
+</template>
+
 <style lang="scss" scoped>
+.pms-tree-aside {
+  width: 100%;
+  height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  min-width: 240px;
+  box-sizing: border-box;
+  flex-shrink: 0;
+}
+
+.pms-tree-aside.is-collapsed {
+  width: 0 !important;
+  min-width: 0 !important;
+  padding: 0 !important;
+  overflow: visible !important;
+  pointer-events: none;
+  box-shadow: none;
+}
+
+:global(.left-tree:has(.pms-tree-aside.is-collapsed)) {
+  width: 0 !important;
+  overflow: visible !important;
+  background: transparent !important;
+}
+
+:global(.left-tree:has(.pms-tree-aside.is-collapsed) + .divider-tree) {
+  display: none;
+}
+
+.tree-body {
+  position: relative;
+  min-height: 0;
+  overflow: hidden;
+  flex: 1;
+}
+
+.tree-node {
+  display: flex;
+  width: 100%;
+  min-width: 0;
+  align-items: center;
+}
+
+.node-icon {
+  margin-right: 6px;
+  fill: currentcolor;
+  flex-shrink: 0;
+}
+
+.node-icon--folder {
+  color: #e6a23c;
+}
+
+.node-icon--device {
+  color: #409eff;
+}
+
+.tree-node__label {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
 .custom-menu {
   position: fixed;
-  background: white;
-  border: 1px solid #ccc;
-  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
-  z-index: 1000;
+  min-width: 144px;
+  padding: 6px;
+  background: #fff;
+  border: 1px solid rgb(15 23 42 / 8%);
+  border-radius: 10px;
+  box-shadow: 0 14px 30px rgb(15 23 42 / 16%);
+  z-index: 3000;
 }
+
 .custom-menu ul {
   list-style: none;
   padding: 0;
   margin: 0;
 }
+
 .custom-menu li {
-  padding: 2px 10px;
+  padding: 8px 12px;
+  margin: 0;
+  font-size: 13px;
+  line-height: 1.2;
+  color: #334155;
   cursor: pointer;
-  font-size: 12px;
-  margin: 2px;
+  border-radius: 8px;
+  transition:
+    background-color 0.18s ease,
+    color 0.18s ease;
 }
+
 .custom-menu li:hover {
-  background: #77a0ec;
+  color: #1677ff;
+  background: #eff6ff;
 }
-.tree-container {
-  overflow-y: auto;
-  min-width: 100%;
-  border: none;
-  border-radius: 4px;
+
+.collapse-handle {
+  position: absolute;
+  top: 50%;
+  right: -14px;
+  z-index: 200;
+  display: flex;
+  width: 14px;
+  height: 60px;
+  color: var(--el-text-color-secondary);
+  pointer-events: auto;
+  cursor: pointer;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-light);
+  border-left: none;
+  border-radius: 0 12px 12px 0;
+  transform: translateY(-50%);
+  box-shadow: 2px 0 6px rgb(0 0 0 / 5%);
+  transition: right 0.3s;
+  align-items: center;
+  justify-content: center;
+}
+
+.is-collapsed .collapse-handle {
+  right: -8px;
+  border-left: 1px solid var(--el-border-color-light);
 }
-/* 自定义滚动条 */
-.tree-container::-webkit-scrollbar {
-  width: 6px;
+
+.collapse-handle:hover {
+  color: var(--el-color-primary);
+  background-color: var(--el-fill-color-light);
+}
+
+:deep(.el-tree) {
+  --el-tree-node-hover-bg-color: var(--el-fill-color-light);
+
+  background: transparent;
 }
-.tree-container::-webkit-scrollbar-thumb {
-  background: #c0c4cc;
-  border-radius: 3px;
+
+:deep(.el-tree-node__content) {
+  height: 32px;
+  border-radius: 4px;
 }
 
-/* 自定义高亮样式 */
-::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
-  background-color: #eaf5ff !important;
-  color: #1890ff !important;
-  font-weight: bold;
+:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
+  font-weight: 600;
+  color: var(--el-color-primary);
+  background-color: var(--el-color-primary-light-9);
 }
 </style>

+ 334 - 362
src/views/pms/qhse/data/index.vue

@@ -1,6 +1,65 @@
 <template>
-  <ContentWrap>
-    <div style="display: flex; justify-content: space-between; align-items: center">
+  <div
+    class="tree-page grid grid-cols-[auto_1fr] grid-rows-[48px_48px_1fr] gap-x-4 gap-y-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <PmsTree
+      class="row-span-3"
+      @node-click="handleFileNodeClick"
+      @success="successList"
+      :currentId="queryParams.classId"
+      :deviceId="id" />
+
+    <el-form
+      class="search-panel bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 gap-8 flex items-center justify-between"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px">
+      <div class="flex items-center gap-8">
+        <el-form-item :label="t('file.name')" prop="filename">
+          <el-input
+            v-model="queryParams.filename"
+            :placeholder="t('file.nameHolder')"
+            size="small"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px" />
+        </el-form-item>
+        <el-form-item v-show="false" :label="t('file.fileType')" prop="fileType">
+          <el-select
+            v-model="queryParams.fileType"
+            :placeholder="t('file.fileTypeHolder')"
+            size="default"
+            clearable
+            class="!w-200px">
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+      </div>
+      <el-form-item>
+        <div class="flex">
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />{{ t('file.search') }}
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />{{ t('file.reset') }}
+          </el-button>
+          <el-button
+            type="primary"
+            :loading="uploadLoading"
+            v-hasPermi="['rq:qhse-info:create']"
+            @click="openForm('create')">
+            <Icon icon="ep:plus" class="mr-5px" />{{ t('file.upload') }}
+          </el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+
+    <div
+      class="breadcrumb-panel bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-4 flex items-center justify-between">
       <el-breadcrumb separator=">" class="breadcrumb-container">
         <el-breadcrumb-item
           v-for="(item, index) in breadcrumbs"
@@ -14,219 +73,199 @@
       <el-input
         v-model="queryParams.allName"
         :placeholder="'在' + breadcrumbs[breadcrumbs.length - 1].name + '下搜索'"
-        style="width: 250px; height: 30px"
+        size="default"
+        style="width: 250px"
         @input="searchFolderAndFile" />
     </div>
-  </ContentWrap>
-  <div class="container-tree" ref="container">
-    <el-row>
-      <div class="left-tree" :style="{ width: leftWidth + 'px' }">
-        <ContentWrapNoBottom>
-          <PmsTree
-            @node-click="handleFileNodeClick"
-            @success="successList"
-            :currentId="queryParams.classId"
-            :deviceId="id" />
-        </ContentWrapNoBottom>
-      </div>
-      <!--    </el-col>-->
-      <div class="divider-tree" @mousedown="startDrag"></div>
-      <div class="right-tree" :style="{ width: rightWidth + 'px' }">
-        <div class="bg-white rounded-lg pt-4 flex items-center">
-          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
-            <el-form-item :label="t('file.name')" prop="filename">
-              <el-input
-                v-model="queryParams.filename"
-                :placeholder="t('file.nameHolder')"
-                clearable
-                @keyup.enter="handleQuery"
-                class="!w-240px" />
-            </el-form-item>
-            <el-form-item v-show="false" :label="t('file.fileType')" prop="fileType">
-              <el-select
-                v-model="queryParams.fileType"
-                :placeholder="t('file.fileTypeHolder')"
-                clearable
-                class="!w-200px">
-                <el-option
-                  v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
-                  :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" /> {{ t('file.search') }}</el-button
-              >
-              <el-button @click="resetQuery"
-                ><Icon icon="ep:refresh" />{{ t('file.reset') }}</el-button
-              >
-              <el-button type="primary" :loading="uploadLoading" @click="openForm('create')">
-                <Icon icon="ep:plus" /> {{ t('file.upload') }}
+
+    <div class="table-panel bg-white dark:bg-[#1d1e1f] rounded-lg shadow overflow-hidden p-3">
+      <zm-table
+        :loading="formLoading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        settings-cache-key="system-tree-file-list"
+        @row-dblclick="inContent"
+        class="custom-table">
+        <zm-table-column :label="t('file.name')" align="left" prop="filename" min-width="300">
+          <template #default="scope">
+            <div style="display: flex; align-items: center; gap: 5px">
+              <Icon v-if="scope.row.fileType === 'content'" icon="fa:folder-open" color="orange" />
+              <Icon
+                v-else-if="scope.row.fileType === 'device'"
+                icon="fa:folder-open"
+                color="blue" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'pic' ||
+                  scope.row.fileClassify === 'jpg' ||
+                  scope.row.fileClassify === 'png' ||
+                  scope.row.fileClassify === 'JPG'
+                "
+                icon="ep:picture-filled"
+                color="#2183D1" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'pdf' || scope.row.fileClassify === 'PDF')
+                "
+                icon="fa-solid:file-pdf"
+                color="#E20012" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'doc' || scope.row.fileClassify === 'docx')
+                "
+                icon="fa:file-word-o"
+                color="blue" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'xls' || scope.row.fileClassify === 'xlsx')
+                "
+                icon="fa-solid:file-excel"
+                color="#107C41" />
+              <Icon
+                v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'txt'"
+                icon="fa:file-text-o" />
+              <Icon
+                v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'mp4'"
+                icon="fa:play-circle-o"
+                color="#009fff" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'ppt' || scope.row.fileClassify === 'pptx')
+                "
+                icon="fa-solid:file-powerpoint"
+                color="#C43E1C" />
+              <Icon v-else icon="fa-solid:file-alt" />
+              {{ scope.row.filename }}
+            </div>
+          </template>
+        </zm-table-column>
+        <zm-table-column :label="t('file.fileType')" align="center" prop="fileType">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.PMS_FILE_TYPE" :value="scope.row.fileType" />
+          </template>
+        </zm-table-column>
+        <zm-table-column :label="t('file.fileSize')" align="center" prop="fileSize" />
+        <zm-table-column :label="t('file.dept')" align="center" prop="deptName" />
+        <!-- <zm-table-column
+          :label="t('file.device')"
+          align="center"
+          prop="deviceCode"
+          min-width="220" /> -->
+        <zm-table-column
+          :label="t('file.operation')"
+          align="center"
+          width="160"
+          fixed="right"
+          action>
+          <template #default="scope">
+            <div class="flex items-center justify-center">
+              <el-button
+                type="primary"
+                v-if="scope.row.fileType !== 'content'"
+                v-hasPermi="['rq:qhse-info:download']"
+                link
+                @click="handleDownload(scope.row.filePath)">
+                <Icon icon="ep:download" />{{ t('file.dow') }}
               </el-button>
-            </el-form-item>
-          </el-form>
-        </div>
-        <ContentWrap style="margin-top: 10px; border: 0">
-          <zm-table
-            :loading="formLoading"
-            :data="list"
-            :stripe="true"
-            :show-overflow-tooltip="true"
-            @row-dblclick="inContent"
-            class="custom-table">
-            <zm-table-column :label="t('file.name')" align="left" prop="filename" min-width="300">
-              <template #default="scope">
-                <div style="display: flex; align-items: center; gap: 5px">
-                  <Icon
-                    v-if="scope.row.fileType === 'content'"
-                    icon="fa:folder-open"
-                    color="orange" />
-                  <Icon
-                    v-else-if="scope.row.fileType === 'device'"
-                    icon="fa:folder-open"
-                    color="blue" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'pic' ||
-                      scope.row.fileClassify === 'jpg' ||
-                      scope.row.fileClassify === 'png' ||
-                      scope.row.fileClassify === 'JPG'
-                    "
-                    icon="ep:picture-filled"
-                    color="#2183D1" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'pdf' || scope.row.fileClassify === 'PDF')
-                    "
-                    icon="fa-solid:file-pdf"
-                    color="#E20012" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'doc' || scope.row.fileClassify === 'docx')
-                    "
-                    icon="fa:file-word-o"
-                    color="blue" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'xls' || scope.row.fileClassify === 'xlsx')
-                    "
-                    icon="fa-solid:file-excel"
-                    color="#107C41" />
-                  <Icon
-                    v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'txt'"
-                    icon="fa:file-text-o" />
-                  <Icon
-                    v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'mp4'"
-                    icon="fa:play-circle-o"
-                    color="#009fff" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'ppt' || scope.row.fileClassify === 'pptx')
-                    "
-                    icon="fa-solid:file-powerpoint"
-                    color="#C43E1C" />
-                  <Icon v-else icon="fa-solid:file-alt" />
-                  {{ scope.row.filename }}
-                </div>
-              </template>
-            </zm-table-column>
-            <zm-table-column :label="t('file.fileType')" align="center" prop="fileType">
-              <template #default="scope">
-                <dict-tag :type="DICT_TYPE.PMS_FILE_TYPE" :value="scope.row.fileType" />
-              </template>
-            </zm-table-column>
-            <zm-table-column :label="t('file.fileSize')" align="center" prop="fileSize" />
-
-            <zm-table-column :label="t('file.dept')" align="center" prop="deptName" />
-            <zm-table-column
-              :label="t('file.device')"
-              align="center"
-              prop="deviceCode"
-              min-width="220" />
-            />
-            <zm-table-column :label="t('file.operation')" align="center" width="160">
-              <template #default="scope">
-                <div class="flex items-center justify-center">
-                  <el-button
-                    type="primary"
-                    v-if="scope.row.fileType !== 'content'"
-                    link
-                    @click="handleDownload(scope.row.filePath)"
-                    v-hasPermi="['rq:iot-info:download']">
-                    <Icon icon="ep:download" />{{ t('file.dow') }}
-                  </el-button>
-                  <el-button
-                    type="danger"
-                    v-if="scope.row.fileType !== 'content'"
-                    link
-                    @click="deleteInfo(scope.row.id)"
-                    v-hasPermi="['rq:iot-info:download']">
-                    <Icon icon="ep:delete" />{{ t('file.delete') }}
-                  </el-button>
-                </div>
-              </template>
-            </zm-table-column>
-          </zm-table>
-        </ContentWrap>
-      </div>
-    </el-row>
+              <el-button
+                type="danger"
+                v-if="scope.row.fileType !== 'content'"
+                link
+                @click="deleteInfo(scope.row.id)"
+                v-hasPermi="['rq:qhse-info:delete']">
+                <Icon icon="ep:delete" />{{ t('file.delete') }}
+              </el-button>
+            </div>
+          </template>
+        </zm-table-column>
+      </zm-table>
+    </div>
   </div>
+
   <IotInfoFormTree
     ref="formRef"
     @success="getList"
     :deviceId="deviceId"
     :nodeId="nodeId"
-    :classId="clickNodeId" />
+    :deptId="deptId"
+    :nodeName="nodeName"
+    :classId="nodeId" />
 </template>
 <script lang="ts" setup>
-import { IotDeviceVO } from '@/api/pms/device'
-import IotInfoFormTree from '@/views/pms/iotinfo/IotInfoFormTree.vue'
-import * as IotInfoApi from '@/api/pms/iotinfo'
-import { ref, onMounted, onUnmounted } from 'vue'
+import IotInfoFormTree from './IotInfoFormTree.vue'
+import * as DataLibraryApi from '@/api/pms/qhse/index'
+import { ref, onMounted } from 'vue'
+import { Base64 } from 'js-base64'
 
-import PmsTree from '@/views/system/tree/PmsTree.vue'
-import { useCache } from '@/hooks/web/useCache'
+import PmsTree from './PmsTree.vue'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { IotInfoClassifyApi } from '@/api/pms/info'
-import { IotTreeApi } from '@/api/system/tree'
+// import { IotTreeApi } from '@/api/system/tree'
 defineOptions({ name: 'IotTree' })
 
-const container = ref(null)
-const leftWidth = ref(350) // 初始左侧宽度
-const rightWidth = ref(window.innerWidth * 0.8)
-let isDragging = false
+type TreeKey = number | string
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  filename: string | null
+  fileType: string | null
+  createTime: string[]
+  deviceId: TreeKey | null
+  classId: TreeKey | null
+  deptId?: TreeKey
+  allName: string | null
+}
+
+interface BreadcrumbItem {
+  id: TreeKey | null
+  name: string
+  type?: string
+}
+
+interface FileRow {
+  id: TreeKey
+  originId?: TreeKey
+  parentId?: TreeKey
+  name?: string
+  filename?: string
+  fileType?: string
+  filePath?: string
+  fileSize?: string
+  fileClassify?: string
+  deptName?: string
+  deviceCode?: string
+  type?: string
+}
+
+interface IotInfoFormTreeExpose {
+  open: (type: string, id?: number) => void
+}
+
 const uploadLoading = ref(false)
+const currentTreeOriginId = ref<TreeKey | undefined>(undefined)
+
+const syncSelectedTreeOriginId = () => {
+  queryParams.deptId = currentTreeOriginId.value
+}
 
 const searchFolderAndFile = async () => {
-  debugger
   formLoading.value = true
-  const data = await IotInfoApi.IotInfoApi.getAllChildContentFile(queryParams)
-  debugger
+  syncSelectedTreeOriginId()
+  const data = await DataLibraryApi.DataLibraryApi.getAllChildContentFile(queryParams)
   list.value = data
   formLoading.value = false
   queryParams.filename = ''
 }
-const openWeb = (url) => {
-  window.open(
-    'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
-  )
-}
-const handleView = (row) => {
-  openForm('detail', row.id)
-}
-const startDrag = (e) => {
-  isDragging = true
-  document.addEventListener('mousemove', onDrag)
-  document.addEventListener('mouseup', stopDrag)
-}
-const topNodeId = ref('')
-const successList = async (id) => {
+const topNodeId = ref<TreeKey | null>(null)
+const successList = async (id?: TreeKey) => {
+  if (id === undefined) return
+
   queryParams.classId = id
   topNodeId.value = id
   const rootItem = breadcrumbs.value.find((item) => item.type === 'root')
@@ -238,34 +277,15 @@ const successList = async (id) => {
   // queryParams.classId = ''
 }
 
-const onDrag = (e) => {
-  if (!isDragging) return
-
-  const containerRect = container.value.getBoundingClientRect()
-  const newWidth = e.clientX - containerRect.left
-
-  // 设置最小和最大宽度限制
-  if (newWidth > 300 && newWidth < containerRect.width - 100) {
-    leftWidth.value = newWidth
-  }
-}
-
-const stopDrag = () => {
-  isDragging = false
-  document.removeEventListener('mousemove', onDrag)
-  document.removeEventListener('mouseup', stopDrag)
-}
-
 const queryFormRef = ref() // 搜索的表单
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const loading = ref(true) // 列表的加载中
 const { params } = useRoute() // 查询参数
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
+const list = ref<FileRow[]>([]) // 列表的数据
 // const total = ref(0) // 列表的总页数
-const id = ref()
-const queryParams = reactive({
+const id = ref(0)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   filename: null,
@@ -276,18 +296,8 @@ const queryParams = reactive({
   deptId: undefined,
   allName: null
 })
-// SPU 表单数据
-const formData = ref({
-  id: undefined,
-  deviceId: undefined,
-  deptId: undefined,
-  filename: undefined,
-  fileType: undefined,
-  filePath: undefined,
-  remark: undefined,
-  classId: undefined
-})
-const handleDownload = async (url) => {
+const handleDownload = async (url?: string) => {
+  if (!url) return
   try {
     formLoading.value = true
     const response = await fetch(url)
@@ -296,7 +306,7 @@ const handleDownload = async (url) => {
 
     const link = document.createElement('a')
     link.href = downloadUrl
-    link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
+    link.download = url.split('/').pop()! // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
     link.click()
 
     URL.revokeObjectURL(downloadUrl)
@@ -305,9 +315,9 @@ const handleDownload = async (url) => {
     console.error('下载失败:', error)
   }
 }
-const deleteInfo = async (id) => {
+const deleteInfo = async (id: TreeKey) => {
   await message.delConfirm()
-  await IotInfoApi.IotInfoApi.deleteIotInfo(id).then((res) => {
+  await DataLibraryApi.DataLibraryApi.deleteIotInfo(Number(id)).then((res) => {
     if (res) {
       message.success('文件删除成功')
       getList()
@@ -316,23 +326,7 @@ const deleteInfo = async (id) => {
     }
   })
 }
-// const handleFileView = (url: string) => {
-//   window.open(
-//     'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
-//   )
-// }
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotInfoApi.IotInfoApi.deleteIotInfo(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-const formRef = ref()
+const formRef = ref<IotInfoFormTreeExpose>()
 const openForm = async (type: string, id?: number) => {
   // if (classType.value==='dept'){
   //   message.error(t('common.deptChoose'))
@@ -341,10 +335,11 @@ const openForm = async (type: string, id?: number) => {
   uploadLoading.value = true
   if (!queryParams.classId) {
     message.error(t('common.leftNode'))
+    uploadLoading.value = false
     return
   }
-  formRef.value.open(type, id)
-  await new Promise((resolve) => {
+  formRef.value?.open(type, id)
+  await new Promise<void>((resolve) => {
     setTimeout(() => {
       resolve() // 操作完成后resolve
     }, 2000) // 模拟2秒耗时
@@ -354,14 +349,15 @@ const openForm = async (type: string, id?: number) => {
 const deviceId = ref('')
 const clickNodeId = ref('')
 const nodeId = ref('')
-const classType = ref('')
+const deptId = ref('')
+const nodeName = ref('')
 
-const breadcrumbs = ref([
+const breadcrumbs = ref<BreadcrumbItem[]>([
   { id: null, name: '科瑞石油技术', type: 'root' } // 根节点
 ])
 
 // 共享的面包屑更新逻辑
-const updateBreadcrumbs = async (node) => {
+const updateBreadcrumbs = async (node: FileRow) => {
   // 查找当前节点是否已在面包屑中
   const currentIndex = breadcrumbs.value.findIndex((item) => item.id === node.id)
 
@@ -370,18 +366,21 @@ const updateBreadcrumbs = async (node) => {
     breadcrumbs.value = breadcrumbs.value.slice(0, currentIndex + 1)
   } else {
     // 新增节点到面包屑
-    breadcrumbs.value.push({ id: node.id, name: node.filename || node.name })
+    breadcrumbs.value.push({ id: node.id, name: node.filename || node.name || '' })
   }
 
   // 更新表格数据
   queryParams.classId = node.id
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  syncSelectedTreeOriginId()
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
 }
 
 // 表格行点击事件
-const inContent = async (row) => {
+const inContent = async (row: FileRow) => {
   if (row.fileType != 'content') {
+    if (!row.filePath) return
+
     window.open(
       'http://doc.deepoil.cc:8012/onlinePreview?url=' +
         encodeURIComponent(Base64.encode(row.filePath))
@@ -394,29 +393,32 @@ const inContent = async (row) => {
   await updateBreadcrumbs(row)
   // 可以添加其他表格行点击需要的逻辑
   queryParams.classId = row.id
-  nodeId.value = row.id
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  nodeId.value = String(row.id)
+  syncSelectedTreeOriginId()
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   formLoading.value = false
   list.value = data
 }
 
 // 文件树节点点击事件
-const handleFileNodeClick = async (row) => {
+const handleFileNodeClick = async (row: FileRow) => {
   queryParams.filename = ''
-  const parentItems = await IotTreeApi.getParentIds(row.id)
+  const parentItems = await DataLibraryApi.DataLibraryApi.getParentIds(row.id)
   breadcrumbs.value = []
   parentItems.forEach((item) => {
     breadcrumbs.value.push({ id: item.id, name: item.name, type: 'root' })
   })
 
   queryParams.classId = row.id
+  currentTreeOriginId.value = row.originId
+  deptId.value = row.originId != null ? String(row.originId) : ''
+  nodeName.value = row.name || row.filename || ''
   if (row.type == 'device') {
-    id.value = row.originId
+    id.value = Number(row.originId)
   }
 
-  classType.value = row.type
   if (row.type === 'device') {
-    deviceId.value = row.originId
+    deviceId.value = String(row.originId)
     const queryParam = {
       deviceId: row.originId,
       pageNo: 1,
@@ -425,25 +427,26 @@ const handleFileNodeClick = async (row) => {
     const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
     if (data) {
       const target = data.filter((item) => item.parentId === 0)
-      clickNodeId.value = target[0].id
+      clickNodeId.value = String(target[0]?.id ?? '')
     }
   } else if (row.type === 'file') {
-    clickNodeId.value = row.originId
+    clickNodeId.value = String(row.originId)
   } else if (row.type === 'dept') {
     // message.error("请选择设备及文件节点")
     // return
   }
-  nodeId.value = row.id
+  nodeId.value = String(row.id)
   await getList()
 }
 
 // 面包屑点击事件
-const handleBreadcrumbClick = async (index) => {
+const handleBreadcrumbClick = async (index: number) => {
   queryParams.filename = ''
-  formLoading.value = true
   // 忽略当前节点的点击
   if (index === breadcrumbs.value.length - 1) return
 
+  formLoading.value = true
+
   // 截断面包屑到点击的节点
   const targetBreadcrumbs = breadcrumbs.value.slice(0, index + 1)
   breadcrumbs.value = targetBreadcrumbs
@@ -453,57 +456,27 @@ const handleBreadcrumbClick = async (index) => {
   if (!targetId) {
     targetId = topNodeId.value
   }
+  if (!targetId) {
+    formLoading.value = false
+    return
+  }
+
   queryParams.classId = targetId
-  clickNodeId.value = targetId
-  nodeId.value = targetId
-  debugger
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  clickNodeId.value = String(targetId)
+  nodeId.value = String(targetId)
+  nodeName.value = targetBreadcrumbs[index].name || ''
+  syncSelectedTreeOriginId()
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
   formLoading.value = false
 }
 
-// const handleFileNodeClick = async (row) => {
-//   queryParams.classId = row.id
-//   classType.value = row.type
-//   if (row.type==='device') {
-//     deviceId.value = row.originId
-//     const queryParam = {
-//       deviceId: row.originId,
-//       pageNo: 1,
-//       pagesize: 10,
-//     }
-//     const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
-//     debugger
-//     if (data){
-//       const target = data.filter((item)=> item.parentId===0)
-//       clickNodeId.value = target[0].id
-//     }
-//   } else if (row.type === 'file'){
-//     clickNodeId.value = row.originId
-//   } else if (row.type==='dept') {
-//     // message.error("请选择设备及文件节点")
-//     // return
-//   }
-//   nodeId.value = row.id
-//   await getList()
-// }
-/** 获得详情 */
-// const getDetail = async () => {
-//   if (id) {
-//     formLoading.value = true
-//     try {
-//       formData.value = (await IotDeviceApi.getIotDevice(id)) as IotDeviceVO
-//     } finally {
-//       formLoading.value = false
-//     }
-//   }
-// }
 /** 查询列表 */
 const getList = async () => {
   formLoading.value = true
   try {
-    const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
-    debugger
+    syncSelectedTreeOriginId()
+    const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
     list.value = data
   } finally {
     formLoading.value = false
@@ -520,90 +493,89 @@ const resetQuery = () => {
   queryFormRef.value?.resetFields()
   handleQuery()
 }
-const { wsCache } = useCache()
+
 /** 初始化 */
 onMounted(async () => {
-  // await getDetail()
-  // queryParams.deptId = wsCache.get(CACHE_KEY.USER).user.deptId;
-  // await getList()
-  // deviceId.value = params.id as unknown as number
-  id.value = params.id as unknown as number
+  id.value = Number(params.id) || 0
 })
 </script>
 <style scoped>
-::v-deep .breadcrumb-container {
-  padding: 12px 16px;
-  background-color: #f5f7fa;
-  border-radius: 6px;
-  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.search-panel .el-form-item) {
+  display: flex;
+  align-items: center;
+}
+
+:deep(.search-panel .el-form-item__label) {
+  height: 32px;
+  line-height: 32px;
+  align-items: center;
 }
-::v-deep .custom-breadcrumb-item {
+
+:deep(.search-panel .el-form-item__content) {
+  display: flex;
+  align-items: center;
+}
+
+.tree-page {
+  min-width: 0;
+}
+
+.search-panel,
+.breadcrumb-panel,
+.table-panel {
+  min-width: 0;
+}
+
+:deep(.breadcrumb-container) {
+  min-width: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+:deep(.custom-breadcrumb-item) {
   font-size: 14px;
-  font-weight: bold;
+  font-weight: 600;
 }
 
-/* 面包屑文本样式 */
-::v-deep .el-breadcrumb__item .el-breadcrumb__inner {
+:deep(.el-breadcrumb__item .el-breadcrumb__inner) {
+  padding: 2px 4px;
   color: #101010;
   text-decoration: none;
-  padding: 2px 4px;
   border-radius: 2px;
   transition: all 0.2s ease;
 }
 
-/* 可点击项悬停效果 */
-::v-deep .el-breadcrumb__item:not(:last-child) .el-breadcrumb__inner:hover {
+:deep(.el-breadcrumb__item:not(:last-child) .el-breadcrumb__inner:hover) {
   color: #409eff;
-  background-color: rgba(64, 158, 255, 0.1);
   cursor: pointer;
+  background-color: rgb(64 158 255 / 10%);
 }
 
-/* 当前项样式 */
-::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
-  color: #1890ff;
+:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
   font-weight: 500;
+  color: #1890ff;
   cursor: default;
 }
 
-/* 分隔符样式优化 */
-::v-deep .el-breadcrumb__separator {
+:deep(.el-breadcrumb__separator) {
   margin: 0 8px;
-  color: #0954f6;
   font-size: 12px;
+  color: #0954f6;
 }
 
-.custom-table {
-  cursor: pointer;
-  --el-table-row-hover-bg-color: #f5f7fa; /* 优化悬停背景色 */
-}
-
-.container-tree {
-  display: flex;
-  height: 100%;
-  user-select: none; /* 防止拖动时选中文本 */
-}
-
-.left-tree {
-  background: #f0f0f0;
-  height: 100%;
-  overflow: auto;
-}
-
-.right-tree {
-  flex: 1;
+:deep(.zm-table) {
   height: 100%;
-  overflow: auto;
-  margin-left: 15px;
-}
 
-.divider-tree {
-  width: 2px;
-  background: #ccc;
-  cursor: col-resize;
-  position: relative;
-}
+  .el-table__cell {
+    height: 42px;
+  }
 
-.divider-tree:hover {
-  background: #666;
+  .el-table__row {
+    cursor: pointer;
+  }
 }
 </style>

+ 11 - 13
src/views/pms/qhse/deviceCert/index.vue

@@ -244,25 +244,25 @@
     </div>
   </div>
 
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <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>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
-          <Icon icon="ep:view" class="mr-2px" />查看</el-button
-        >
+          <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
-        >
+          <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>
+        <el-button type="primary" @click="dialogFileView = false">确认</el-button>
       </div>
     </template>
   </Dialog>
@@ -521,12 +521,10 @@ onMounted(() => {
 
 <style scoped>
 .file-name-text {
-  flex: 1;
+  display: block;
+  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: nowrap;
-  margin-right: 12px;
-  color: var(--el-text-color-primary);
 }
 
 .stats-cards {

+ 6 - 8
src/views/pms/qhse/emergencyDrill/bookCheck/index.vue

@@ -132,13 +132,13 @@
     </div>
   </div>
 
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <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>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
           <Icon icon="ep:view" class="mr-2px" />查看
         </el-button>
@@ -316,11 +316,9 @@ onMounted(async () => {
 
 <style scoped>
 .file-name-text {
-  flex: 1;
+  display: block;
+  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: nowrap;
-  margin-right: 12px;
-  color: var(--el-text-color-primary);
 }
 </style>

+ 6 - 8
src/views/pms/qhse/emergencyDrill/index.vue

@@ -157,13 +157,13 @@
     </div>
   </div>
 
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <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>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
           <Icon icon="ep:view" class="mr-2px" />查看
         </el-button>
@@ -321,11 +321,9 @@ onMounted(async () => {
 
 <style scoped>
 .file-name-text {
-  flex: 1;
+  display: block;
+  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: nowrap;
-  margin-right: 12px;
-  color: var(--el-text-color-primary);
 }
 </style>

+ 11 - 12
src/views/pms/qhse/hazard/index.vue

@@ -118,18 +118,17 @@
           <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>
+              <el-button
+                text
+                :underline="false"
+                size="small"
+                type="primary"
+                @click="openForm('edit', row)">
+                编辑
+              </el-button>
+              <el-button text :underline="false" size="small" type="danger" @click="deleteRow(row)">
+                删除
+              </el-button>
             </template>
           </zm-table-column>
         </zm-table>

+ 91 - 13
src/views/pms/qhse/index.vue

@@ -139,7 +139,7 @@
               <zm-table-column label="编码" align="center" prop="measureCode" fixed="left" />
               <zm-table-column label="序列号" align="center" prop="serialNo" 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="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" />
@@ -148,6 +148,16 @@
                   <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
                 </template>
               </zm-table-column>
+              <zm-table-column label="状态" align="center" prop="status" min-width="100">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.MEASURE_STATUS" :value="scope.row.status" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="报废时间" align="center" prop="offTime" min-width="120">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.offTime) }}
+                </template>
+              </zm-table-column>
               <zm-table-column label="采购日期" align="center" prop="buyDate">
                 <template #default="scope">
                   {{ formatDateCorrectly(scope.row.buyDate) }}
@@ -282,8 +292,8 @@
 
       <el-row :gutter="20">
         <el-col :span="12">
-          <el-form-item label="计量单位" prop="measureUnit">
-            <el-input v-model="formData.measureUnit" placeholder="请输入计量单位" />
+          <el-form-item label="精度" prop="measureUnit">
+            <el-input v-model="formData.measureUnit" placeholder="请输入精度" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -313,6 +323,37 @@
         </el-col> -->
       </el-row>
 
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="formData.status"
+              placeholder="请选择状态"
+              clearable
+              class="!w-240px">
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.MEASURE_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="isScrappedStatus(formData.status)">
+          <div class="flex items-center relative">
+            <span class="text-red absolute left-6 top-[-1px]">*</span>
+            <el-form-item label="报废时间" prop="offTime">
+              <el-date-picker
+                v-model="formData.offTime"
+                type="date"
+                value-format="x"
+                placeholder="请选择报废时间"
+                style="width: 100%" />
+            </el-form-item>
+          </div>
+        </el-col>
+      </el-row>
+
       <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="价格" prop="measurePrice">
@@ -432,25 +473,25 @@
     </div>
   </el-drawer>
 
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <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>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
-          <Icon icon="ep:view" class="mr-2px" />查看</el-button
-        >
+          <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
-        >
+          <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>
+        <el-button type="primary" @click="dialogFileView = false">确认</el-button>
       </div>
     </template>
   </Dialog>
@@ -539,6 +580,8 @@ const formData = ref({
   brand: '',
   modelName: '',
   classify: '',
+  status: '',
+  offTime: null,
   buyDate: null,
   remark: '',
   deptId: '',
@@ -563,15 +606,31 @@ const formatDateCorrectly = (timestamp) => {
   return formatDate(time).substring(0, 10)
 }
 
+const isScrappedStatus = (status) => {
+  const option = getStrDictOptions(DICT_TYPE.MEASURE_STATUS).find((item) => item.value === status)
+  return option?.label === '报废'
+}
+
+const validateOffTime = (_rule, value, callback) => {
+  if (isScrappedStatus(formData.value.status) && !value) {
+    callback(new Error('报废状态必须填写报废时间'))
+    return
+  }
+  callback()
+}
+
 // 表单验证规则
 const formRules = {
   measureName: [{ required: true, message: '计量器具名称不能为空', trigger: 'blur' }],
   dutyPerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
   classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
   deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
   validity: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
-  serialNo: [{ required: true, message: '序列号不能为空', trigger: 'blur' }]
+  serialNo: [{ required: true, message: '序列号不能为空', trigger: 'blur' }],
+  measureUnit: [{ required: true, message: '精度不能为空', trigger: 'blur' }],
+  offTime: [{ validator: validateOffTime, trigger: ['change', 'blur'] }]
 }
 
 /** 查询列表 */
@@ -687,6 +746,7 @@ const handleEdit = (row) => {
   formData.value = {
     ...row,
     // 确保日期字段正确处理
+    offTime: row.offTime ? ensureMillisecondTimestamp(row.offTime) : null,
     buyDate: row.buyDate ? ensureMillisecondTimestamp(row.buyDate) : null,
     validity: row.validity ? ensureMillisecondTimestamp(row.validity) : null
   }
@@ -800,6 +860,8 @@ const resetForm = () => {
     brand: '',
     modelName: '',
     classify: '',
+    status: '',
+    offTime: null,
     buyDate: null,
     remark: '',
     deptId: '',
@@ -818,6 +880,15 @@ const closeDialog = () => {
   resetForm()
 }
 
+watch(
+  () => formData.value.status,
+  (value) => {
+    if (!isScrappedStatus(value)) {
+      formData.value.offTime = null
+    }
+  }
+)
+
 // 提交表单
 const submitForm = async () => {
   if (!formRef.value) return
@@ -830,6 +901,7 @@ const submitForm = async () => {
     const submitData = {
       ...formData.value,
       // 确保日期字段以正确的格式提交
+      offTime: formData.value.offTime,
       buyDate: formData.value.buyDate,
       validity: formData.value.validity
     }
@@ -1030,6 +1102,12 @@ onUnmounted(() => {
 </script>
 
 <style scoped>
+.file-name-text {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
 ::deep(.el-tree--highlight-current) {
   height: 200px !important;
 }

+ 26 - 2
src/views/pms/qhse/iotmeasuredetect/IotMeasureDetectForm.vue

@@ -69,6 +69,11 @@
       </el-row>
 
       <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="精度" prop="precisions">
+            <el-input v-model="formData.precisions" placeholder="请输入精度" />
+          </el-form-item>
+        </el-col>
         <el-col :span="12">
           <el-form-item label="校准金额" prop="detectAmount">
             <el-input v-model="formData.detectAmount" placeholder="请输入校准金额" />
@@ -76,6 +81,20 @@
         </el-col>
       </el-row>
 
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="检测周期" prop="detectCycle">
+            <el-select v-model="formData.detectCycle" placeholder="检测周期" clearable>
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.QHSE_DETECT_CYCLE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
       <el-row :gutter="20">
         <el-col :span="24">
           <el-form-item label="检测/校准内容" prop="detectContent">
@@ -228,6 +247,7 @@ import { formatDate } from '@/utils/formatTime'
 import { handleTree } from '@/utils/tree'
 import { onMounted, ref } from 'vue'
 import * as DeptApi from '@/api/system/dept'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 
 /** 计量器具-检测校准明细 表单 */
 defineOptions({ name: 'IotMeasureDetectForm' })
@@ -252,7 +272,9 @@ const formData = ref({
   measureName: '',
   measureId: '',
   measureCertNo: '',
-  file: ''
+  file: '',
+  precisions: '',
+  detectCycle: ''
 })
 const formRules = reactive({
   measureId: [{ required: true, message: '计量器具不能为空', trigger: 'blur' }],
@@ -262,7 +284,9 @@ const formRules = reactive({
   validityPeriod: [{ required: true, message: '检测/校准有效期不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
   detectStandard: [{ required: true, message: '检测/校准标准不能为空', trigger: 'blur' }],
-  file: [{ required: true, message: '请上传文件', trigger: 'blur' }]
+  file: [{ required: true, message: '请上传文件', trigger: 'blur' }],
+  precisions: [{ required: true, message: '精度不能为空', trigger: 'blur' }],
+  detectCycle: [{ required: true, message: '请选择检测周期', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const measureList = ref([])

+ 8 - 1
src/views/pms/qhse/iotmeasuredetect/index.vue

@@ -125,6 +125,12 @@
                   }}</span>
                 </template>
               </zm-table-column>
+              <zm-table-column label="检测周期" align="center" width="140">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.QHSE_DETECT_CYCLE" :value="scope.row.detect_cycle" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="精度" align="center" prop="precisions" />
               <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">
@@ -162,7 +168,7 @@
     </div>
   </div>
 
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <div
       v-for="(file, index) in fileList"
       :key="index"
@@ -206,6 +212,7 @@ import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE } from '@/utils/dict'
 
 /** 计量器具-检测校准明细 列表 */
 defineOptions({ name: 'IotMeasureDetect' })

+ 29 - 27
src/views/pms/qhse/jsa/index.vue

@@ -13,8 +13,7 @@
               placeholder="请输入序号"
               clearable
               @keyup.enter="handleQuery"
-              class="!w-150px"
-            />
+              class="!w-150px" />
           </el-form-item>
 
           <el-form-item label="工作负责人" prop="jobDuty">
@@ -23,8 +22,7 @@
               placeholder="请输入工作负责人"
               clearable
               @keyup.enter="handleQuery"
-              class="!w-150px"
-            />
+              class="!w-150px" />
           </el-form-item>
 
           <!-- <el-form-item label="工作地点" prop="jobAddress">
@@ -47,8 +45,7 @@
               type="primary"
               plain
               @click="openForm('create')"
-              v-hasPermi="['rq:qhse-jsa:create']"
-            >
+              v-hasPermi="['rq:qhse-jsa:create']">
               <Icon icon="ep:plus" class="mr-5px" /> 新增
             </el-button>
             <el-button
@@ -56,8 +53,7 @@
               plain
               @click="handleExport"
               :loading="exportLoading"
-              v-hasPermi="['rq:qhse-jsa:export']"
-            >
+              v-hasPermi="['rq:qhse-jsa:export']">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -89,13 +85,14 @@
 
           <zm-table-column label="附件" align="center" prop="jsaFile" min-width="90">
             <template #default="{ row }">
-              <el-link
+              <el-button
+                text
                 v-if="row.jsaFile"
                 :underline="false"
                 type="primary"
                 size="small"
                 @click="viewFile(row.jsaFile)"
-                >查看</el-link
+                >查看</el-button
               >
               <span v-else>-</span>
             </template>
@@ -116,16 +113,14 @@
                 link
                 type="primary"
                 @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:qhse-jsa:update']"
-              >
+                v-hasPermi="['rq:qhse-jsa:update']">
                 编辑
               </el-button>
               <el-button
                 link
                 type="danger"
                 @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:qhse-jsa:delete']"
-              >
+                v-hasPermi="['rq:qhse-jsa:delete']">
                 删除
               </el-button>
             </template>
@@ -136,33 +131,31 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+          @pagination="getList" />
       </ContentWrap>
 
-      <el-dialog v-model="dialogFileView" title="附件" width="500">
+      <Dialog v-model="dialogFileView" title="附件">
         <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>
+          class="mt-5 flex min-w-0 items-center justify-between gap-5">
+          <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+          <div class="flex shrink-0 items-center gap-5 flex-nowrap">
             <el-button link type="primary" @click="viewFileInfo(file)">
-              <Icon icon="ep:view" class="mr-2px" />查看</el-button
-            >
+              <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
-            >
+              <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>
+            <el-button type="primary" @click="dialogFileView = false">确认</el-button>
           </div>
         </template>
-      </el-dialog>
+      </Dialog>
     </el-col>
   </el-row>
 
@@ -311,3 +304,12 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<style scoped>
+.file-name-text {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+</style>

+ 632 - 18
src/views/pms/qhse/kanban/index.vue

@@ -20,6 +20,7 @@ import {
 import {
   kanbanApi,
   IotDangerApi,
+  IotHiddenApi,
   IotInstrumentApi,
   IotMeasureCertApi,
   IotCertificateApi
@@ -207,8 +208,8 @@ const permitStats = ref<PermitStat[]>([
 ])
 
 const qualificationWarnings = ref([
-  { label: '证件过期', value: 0, accent: '#ff7a7a' },
-  { label: '即将到期', value: 0, accent: '#e6ab00' }
+  { label: '证件过期', value: 0, accent: '#ff7a7a', key: 'expired' },
+  { label: '即将到期', value: 0, accent: '#e6ab00', key: 'warn' }
 ])
 
 const qhseMetricTabs = [
@@ -223,6 +224,7 @@ const qhseMetricTabs = [
 ] as const
 
 const activeQhseMetric = ref<QhseMetricValue>('ltir')
+const dangerGradeOptions = computed(() => getStrDictOptions(DICT_TYPE.DANGER_GRADE))
 
 const qhseTrendSeries = ref<Record<QhseMetricValue, Array<{ year: string; value: number }>>>({
   ltir: [
@@ -299,7 +301,7 @@ const bottomCards = ref([
     lines: ['职业健康体检率', '98.7%(达标)']
   },
   {
-    title: '环境危废',
+    title: '危废管控',
     icon: Flag,
     accent: '#28c98b',
     glow: 'rgba(40, 201, 139, 0.2)',
@@ -355,12 +357,19 @@ const safeDayBarData = ref<SafeDayBarItem[]>([])
 const summaryPanel = ref<any>(null)
 const total = ref(0)
 const instrumentExpired = ref(0)
+const safeDayDisplayOrder = ['瑞气能源', '瑞霖技术', '5#项目', '酸化压裂', '气体增产', '钻修井']
 
 function buildSafeDayBarData(baseData: SafeDayMap) {
-  safeDayBarData.value = Object.entries(baseData || {}).map(([name, value]) => ({
+  const mappedData = Object.entries(baseData || {}).map(([name, value]) => ({
     name,
     value: Number(value) || 0
   }))
+  const orderedData = safeDayDisplayOrder
+    .map((name) => mappedData.find((item) => item.name === name))
+    .filter(Boolean) as SafeDayBarItem[]
+  const remainingData = mappedData.filter((item) => !safeDayDisplayOrder.includes(item.name))
+
+  safeDayBarData.value = [...orderedData, ...remainingData]
 }
 
 async function getStatic() {
@@ -503,6 +512,8 @@ function initHazardChart() {
     renderer: CHART_RENDERER
   })
   hazardChart.setOption(getHazardChartOption(), true)
+  hazardChart.off('click')
+  hazardChart.on('click', handleHazardBarClick)
 }
 
 function updateHazardChart() {
@@ -574,6 +585,7 @@ function getSafeDayChartOption(): echarts.EChartsOption {
     },
     yAxis: {
       type: 'category',
+      inverse: true,
       data: safeDayBarData.value.map((item) => item.name),
       axisLine: { show: false },
       axisTick: { show: false },
@@ -946,6 +958,72 @@ const certQueryParams = reactive({
   alertWarn: undefined as string | undefined
 })
 
+const certWarningDialogVisible = ref(false)
+const certWarningDialogTitle = ref('')
+const certWarningLoading = ref(false)
+const certWarningList = ref<any[]>([])
+const certWarningTotal = ref(0)
+const certWarningQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: userStore.getUser.deptId,
+  type: undefined as string | undefined,
+  classify: undefined as string | undefined,
+  userName: undefined as string | undefined,
+  expired: undefined as boolean | undefined,
+  alertWarn: undefined as boolean | undefined
+})
+
+const hazardDialogVisible = ref(false)
+const hazardDialogTitle = ref('')
+const hazardDialogLoading = ref(false)
+const hazardDialogList = ref<any[]>([])
+const hazardDialogTotal = ref(0)
+const hazardDialogQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: userStore.getUser.deptId,
+  status: undefined as string | undefined,
+  address: '',
+  source: ''
+})
+
+const dangerDialogVisible = ref(false)
+const dangerDialogTitle = ref('')
+const dangerDialogLoading = ref(false)
+const dangerDialogList = ref<any[]>([])
+const dangerDialogTotal = ref(0)
+const dangerDialogQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  riskGrade: '',
+  deptId: userStore.getUser.deptId
+})
+
+const instrumentDialogVisible = ref(false)
+const instrumentDialogTitle = ref('')
+const instrumentDialogLoading = ref(false)
+const instrumentDialogList = ref<any[]>([])
+const instrumentDialogTotal = ref(0)
+const instrumentDialogQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  measureName: undefined as string | undefined,
+  measureCertNo: undefined as string | undefined,
+  deptId: userStore.getUser.deptId,
+  expired: undefined as boolean | undefined,
+  alertWarn: undefined as boolean | undefined
+})
+
+const getCertificateTypeText = (type: string) => {
+  const map: Record<string, string> = {
+    personal: '个人证书',
+    organization: '组织证书',
+    other: '其他'
+  }
+  return map[type] || type
+}
+
 function formatDateCorrectly(timestamp: any) {
   if (!timestamp) return ''
   let time = Number(timestamp)
@@ -1016,6 +1094,134 @@ async function getCertificateBoardList() {
   }
 }
 
+async function getCertificateWarningList() {
+  certWarningLoading.value = true
+  try {
+    const data = await IotMeasureCertApi.getIotMeasureCertPage(certWarningQueryParams)
+    certWarningList.value = data.list || []
+    certWarningTotal.value = data.total || 0
+  } finally {
+    certWarningLoading.value = false
+  }
+}
+
+async function getHazardDialogList() {
+  hazardDialogLoading.value = true
+  try {
+    const data = await IotHiddenApi.getHiddenList(hazardDialogQueryParams)
+    hazardDialogList.value = data.list || []
+    hazardDialogTotal.value = data.total || 0
+  } finally {
+    hazardDialogLoading.value = false
+  }
+}
+
+async function getDangerDialogList() {
+  dangerDialogLoading.value = true
+  try {
+    const data = await IotDangerApi.getDangerList(dangerDialogQueryParams)
+    dangerDialogList.value = data.list || []
+    dangerDialogTotal.value = data.total || 0
+  } finally {
+    dangerDialogLoading.value = false
+  }
+}
+
+async function getInstrumentDialogList() {
+  instrumentDialogLoading.value = true
+  try {
+    const data = await IotInstrumentApi.getInstrumentList(instrumentDialogQueryParams)
+    instrumentDialogList.value = data.list || []
+    instrumentDialogTotal.value = data.total || 0
+  } finally {
+    instrumentDialogLoading.value = false
+  }
+}
+
+function openHazardDialog(barLabel: string) {
+  hazardDialogQueryParams.pageNo = 1
+  hazardDialogQueryParams.pageSize = 10
+  hazardDialogQueryParams.deptId = userStore.getUser.deptId
+  hazardDialogQueryParams.address = ''
+  hazardDialogQueryParams.source = ''
+
+  if (barLabel === '未整改') {
+    hazardDialogTitle.value = '未整改隐患列表'
+    hazardDialogQueryParams.status = 'todo'
+  } else if (barLabel === '已整改') {
+    hazardDialogTitle.value = '已整改隐患列表'
+    hazardDialogQueryParams.status = 'finished'
+  } else {
+    hazardDialogTitle.value = '隐患总数列表'
+    hazardDialogQueryParams.status = undefined
+  }
+
+  hazardDialogVisible.value = true
+  getHazardDialogList()
+}
+
+function openDangerDialog(zone: { title?: string }) {
+  const riskGradeLabel = String(zone.title || '')
+    .replace(/区$/, '')
+    .trim()
+  const matchedOption = dangerGradeOptions.value.find((item) => item.label === riskGradeLabel)
+  const riskGrade = matchedOption?.value || ''
+  dangerDialogQueryParams.pageNo = 1
+  dangerDialogQueryParams.pageSize = 10
+  dangerDialogQueryParams.deptId = userStore.getUser.deptId
+  dangerDialogQueryParams.riskGrade = riskGrade
+  dangerDialogTitle.value = riskGradeLabel ? `${riskGradeLabel}风险列表` : '风险列表'
+  dangerDialogVisible.value = true
+  getDangerDialogList()
+}
+
+function openInstrumentDialog(type: 'all' | 'expired') {
+  instrumentDialogQueryParams.pageNo = 1
+  instrumentDialogQueryParams.pageSize = 10
+  instrumentDialogQueryParams.measureName = undefined
+  instrumentDialogQueryParams.measureCertNo = undefined
+  instrumentDialogQueryParams.deptId = userStore.getUser.deptId
+  instrumentDialogQueryParams.alertWarn = undefined
+
+  if (type === 'expired') {
+    instrumentDialogTitle.value = '待检设备列表'
+    instrumentDialogQueryParams.expired = true
+  } else {
+    instrumentDialogTitle.value = '在用设备列表'
+    instrumentDialogQueryParams.expired = undefined
+  }
+
+  instrumentDialogVisible.value = true
+  getInstrumentDialogList()
+}
+
+function handleHazardBarClick(params: { name?: string }) {
+  if (!params?.name) return
+  openHazardDialog(params.name)
+}
+
+function openCertificateWarningDialog(type: 'expired' | 'warn') {
+  certWarningQueryParams.pageNo = 1
+  certWarningQueryParams.pageSize = 10
+  certWarningQueryParams.deptId = userStore.getUser.deptId
+  certWarningQueryParams.type = undefined
+  certWarningQueryParams.classify = undefined
+  certWarningQueryParams.userName = undefined
+
+  if (type === 'expired') {
+    certWarningDialogTitle.value = '证件过期'
+    certWarningQueryParams.expired = true
+    certWarningQueryParams.alertWarn = undefined
+  } else {
+    certWarningDialogTitle.value = '证件即将到期'
+    certWarningQueryParams.expired = undefined
+    certWarningQueryParams.alertWarn = true
+  }
+
+  certWarningDialogVisible.value = true
+  getCertificateWarningList()
+}
+
 function handleCertificateQuery() {
   certQueryParams.pageNo = 1
   getCertificateBoardList()
@@ -1182,7 +1388,11 @@ onUnmounted(() => {
                   风险管控及隐患治理
                 </div>
                 <div class="risk-grid">
-                  <article v-for="zone in riskZones" :key="zone.title" class="risk-card">
+                  <article
+                    v-for="zone in riskZones"
+                    :key="zone.title"
+                    class="risk-card risk-card--clickable"
+                    @click="openDangerDialog(zone)">
                     <div class="risk-card__title">
                       <span class="risk-card__dot" :style="{ background: zone.color }"></span>
                       <span :style="{ color: zone.color }">{{ zone.title }}</span>
@@ -1262,7 +1472,8 @@ onUnmounted(() => {
                       <div
                         v-for="item in qualificationWarnings"
                         :key="item.label"
-                        class="qualification-item">
+                        class="qualification-item qualification-item--clickable"
+                        @click="openCertificateWarningDialog(item.key)">
                         <span class="qualification-item__label">{{ item.label }}:</span>
                         <strong :style="{ color: item.accent }">
                           <CountTo
@@ -1302,7 +1513,23 @@ onUnmounted(() => {
                 <div class="bottom-card__title">{{ card.title }}</div>
               </div>
               <div class="bottom-card__content">
-                <p v-for="line in card.lines" :key="line">{{ line }}</p>
+                <template v-if="card.title === '安全检测'">
+                  <button
+                    type="button"
+                    class="bottom-card__action"
+                    @click="openInstrumentDialog('all')">
+                    {{ card.lines[0] }}
+                  </button>
+                  <button
+                    type="button"
+                    class="bottom-card__action"
+                    @click="openInstrumentDialog('expired')">
+                    {{ card.lines[1] }}
+                  </button>
+                </template>
+                <template v-else>
+                  <p v-for="line in card.lines" :key="line">{{ line }}</p>
+                </template>
               </div>
             </article>
           </section>
@@ -1401,6 +1628,17 @@ onUnmounted(() => {
               </el-table-column>
 
               <el-table-column label="备注" align="center" prop="remark" />
+              <el-table-column label="操作" align="center" fixed="right" min-width="180px" action>
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="success"
+                    v-if="scope.row.certPic"
+                    @click="viewFile(scope.row.certPic)">
+                    查看证书
+                  </el-button>
+                </template>
+              </el-table-column>
             </el-table>
 
             <div class="certificate-pagination">
@@ -1431,35 +1669,356 @@ onUnmounted(() => {
     </div>
   </div>
 
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <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>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
-          <Icon icon="ep:view" class="mr-2px" />查看</el-button
-        >
+          <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
-        >
+          <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>
+        <el-button type="primary" @click="dialogFileView = false">确认</el-button>
       </div>
     </template>
   </Dialog>
+
+  <Dialog v-model="certWarningDialogVisible" :title="certWarningDialogTitle" width="1100px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="certWarningLoading"
+        :data="certWarningList"
+        :row-style="tableRowStyle"
+        :row-class-name="tableRowClassName"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (certWarningQueryParams.pageNo - 1) * certWarningQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="证书类型" align="center" prop="type" width="120">
+          <template #default="scope">
+            {{ getCertificateTypeText(scope.row.type) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="证书类别" align="center" width="150" prop="classify">
+          <template #default="scope">
+            <dict-tag
+              v-if="scope.row.type === 'organization'"
+              :type="DICT_TYPE.ORG_CERT"
+              :value="scope.row.classify" />
+            <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="证书名称"
+          align="center"
+          prop="certName"
+          show-overflow-tooltip
+          width="100" />
+        <el-table-column label="所属人" align="center" prop="userName" width="100" />
+        <el-table-column
+          label="所在部门"
+          align="center"
+          prop="deptName"
+          width="100"
+          show-overflow-tooltip />
+        <el-table-column
+          label="颁发机构"
+          align="center"
+          prop="certOrg"
+          width="180"
+          show-overflow-tooltip />
+        <el-table-column
+          label="证书标准"
+          align="center"
+          prop="certStandard"
+          width="100"
+          show-overflow-tooltip />
+        <el-table-column label="颁发时间" align="center" prop="certIssue" width="120">
+          <template #default="scope">
+            {{ formatDateCorrectly(scope.row.certIssue) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="有效期" align="center" prop="certExpire" width="120">
+          <template #default="scope">
+            {{ formatDateCorrectly(scope.row.certExpire) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" fixed="right" min-width="100px">
+          <template #default="scope">
+            <el-button
+              link
+              type="success"
+              v-if="scope.row.certPic"
+              @click="viewFile(scope.row.certPic)">
+              查看证书
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="certWarningTotal"
+          v-model:page="certWarningQueryParams.pageNo"
+          v-model:limit="certWarningQueryParams.pageSize"
+          @pagination="getCertificateWarningList" />
+      </div>
+    </div>
+  </Dialog>
+
+  <Dialog v-model="hazardDialogVisible" :title="hazardDialogTitle" width="1400px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="hazardDialogLoading"
+        :data="hazardDialogList"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (hazardDialogQueryParams.pageNo - 1) * hazardDialogQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="单位" align="center" prop="deptName" min-width="140" />
+        <el-table-column label="类型" align="center" prop="classifyName" min-width="120" />
+        <el-table-column label="来源" align="center" min-width="110">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.QHSE_HAZARD_SOURCE" :value="scope.row.source" />
+          </template>
+        </el-table-column>
+        <el-table-column label="地点" align="center" prop="address" min-width="140" />
+        <el-table-column
+          label="存在问题"
+          align="center"
+          prop="problem"
+          min-width="180"
+          show-overflow-tooltip />
+        <el-table-column label="隐患附件" align="center" min-width="100">
+          <template #default="{ row }">
+            <el-button v-if="row.hazardFile" link type="primary" @click="viewFile(row.hazardFile)">
+              查看
+            </el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="整改人"
+          align="center"
+          prop="correctPersonName"
+          min-width="100"
+          show-overflow-tooltip />
+        <el-table-column
+          label="整改要求"
+          align="center"
+          prop="correctStandard"
+          min-width="160"
+          show-overflow-tooltip />
+        <el-table-column
+          label="整改情况"
+          align="center"
+          prop="rectifyDesc"
+          min-width="160"
+          show-overflow-tooltip />
+        <el-table-column label="整改附件" align="center" min-width="100">
+          <template #default="{ row }">
+            <el-button
+              v-if="row.rectifyFile"
+              link
+              type="primary"
+              @click="viewFile(row.rectifyFile)">
+              查看
+            </el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="排查时间" align="center" min-width="120">
+          <template #default="{ row }">
+            {{ formatDateCorrectly(row.checkTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" min-width="90">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.QHSE_HAZARD_STATUS" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="备注"
+          align="center"
+          prop="remark"
+          min-width="120"
+          show-overflow-tooltip />
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="hazardDialogTotal"
+          v-model:page="hazardDialogQueryParams.pageNo"
+          v-model:limit="hazardDialogQueryParams.pageSize"
+          @pagination="getHazardDialogList" />
+      </div>
+    </div>
+  </Dialog>
+
+  <Dialog v-model="dangerDialogVisible" :title="dangerDialogTitle" width="1400px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="dangerDialogLoading"
+        :data="dangerDialogList"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (dangerDialogQueryParams.pageNo - 1) * dangerDialogQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="区域/位置"
+          align="center"
+          prop="region"
+          min-width="140"
+          show-overflow-tooltip />
+        <el-table-column
+          label="危险因素描述"
+          align="center"
+          prop="elementDescription"
+          min-width="180"
+          show-overflow-tooltip />
+        <el-table-column
+          label="可导致的后果"
+          align="center"
+          prop="maybeResult"
+          min-width="180"
+          show-overflow-tooltip />
+        <el-table-column label="风险评价" align="center">
+          <el-table-column prop="evalKn" label="可能性 (L)" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.evalKn }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="evalYz" label="严重性 (S)" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.evalYz }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="evalFxz" label="风险值 (R)" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.evalFxz }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="riskGrade" label="风险等级" width="100" align="center">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.DANGER_GRADE" :value="scope.row.riskGrade" />
+            </template>
+          </el-table-column>
+        </el-table-column>
+        <el-table-column
+          label="控制措施"
+          align="center"
+          prop="controlMethod"
+          show-overflow-tooltip />
+        <el-table-column
+          label="责任人"
+          align="center"
+          prop="charge"
+          min-width="100"
+          show-overflow-tooltip />
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="dangerDialogTotal"
+          v-model:page="dangerDialogQueryParams.pageNo"
+          v-model:limit="dangerDialogQueryParams.pageSize"
+          @pagination="getDangerDialogList" />
+      </div>
+    </div>
+  </Dialog>
+
+  <Dialog v-model="instrumentDialogVisible" :title="instrumentDialogTitle" width="1400px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="instrumentDialogLoading"
+        :data="instrumentDialogList"
+        :row-style="tableRowStyle"
+        :row-class-name="tableRowClassName"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (instrumentDialogQueryParams.pageNo - 1) * instrumentDialogQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="名称" align="center" prop="measureName" min-width="140" />
+        <el-table-column label="编码" align="center" prop="measureCode" min-width="120" />
+        <el-table-column label="序列号" align="center" prop="serialNo" min-width="120" />
+        <el-table-column label="部门名称" align="center" prop="deptName" min-width="140" />
+        <el-table-column label="计量单位" align="center" prop="measureUnit" min-width="100" />
+        <el-table-column label="责任人" align="center" prop="dutyPerson" min-width="100" />
+        <el-table-column label="品牌" align="center" prop="brand" min-width="100" />
+        <el-table-column label="规格型号" align="center" prop="modelName" min-width="120" />
+        <el-table-column label="分类" align="center" min-width="120">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
+          </template>
+        </el-table-column>
+        <el-table-column label="采购日期" align="center" min-width="120">
+          <template #default="scope">
+            {{ formatDateCorrectly(scope.row.buyDate) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="价格" align="center" prop="measurePrice" min-width="100" />
+        <el-table-column
+          label="备注"
+          align="center"
+          prop="remark"
+          min-width="140"
+          show-overflow-tooltip />
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="instrumentDialogTotal"
+          v-model:page="instrumentDialogQueryParams.pageNo"
+          v-model:limit="instrumentDialogQueryParams.pageSize"
+          @pagination="getInstrumentDialogList" />
+      </div>
+    </div>
+  </Dialog>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/qhse_kb.scss');
 
 :deep(.el-select__wrapper) {
-  height: 38px !important;
+  // height: 38px !important;
   font-size: 14px !important;
 }
 .qhse-board {
@@ -1529,6 +2088,36 @@ onUnmounted(() => {
   color: #5c7393;
 }
 
+.bottom-card__action {
+  display: block;
+  width: 100%;
+  padding: 0;
+  text-align: left;
+  color: #617491;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  transition: color 0.2s ease;
+  font-size: 18px;
+  font-weight: 700;
+  color: #5c7393;
+}
+
+.bottom-card__action:hover {
+  color: #2d78ea;
+}
+
+.risk-card--clickable {
+  cursor: pointer;
+  transition:
+    transform 0.2s ease,
+    box-shadow 0.2s ease;
+}
+
+.risk-card--clickable:hover {
+  transform: translateY(-2px);
+}
+
 .summary-toolbar__picker {
   width: 190px;
 }
@@ -1820,10 +2409,28 @@ onUnmounted(() => {
   color: #516785;
 }
 
+.qualification-item--clickable {
+  cursor: pointer;
+  transition:
+    transform 0.18s ease,
+    opacity 0.18s ease;
+}
+
+.qualification-item--clickable:hover {
+  transform: translateX(4px);
+  opacity: 0.9;
+}
+
 .qualification-item strong {
   font-size: 28px;
 }
 
+.certificate-warning-dialog {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
 .bottom-grid {
   display: grid;
   margin-top: 10px;
@@ -1940,6 +2547,13 @@ onUnmounted(() => {
   flex: 1;
 }
 
+.file-name-text {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 // :deep(.el-input__inner) {
 //   font-size: 14px !important;
 //   border: none !important;
@@ -1968,7 +2582,7 @@ onUnmounted(() => {
 }
 
 .certificate-pagination {
-  padding-top: 14px;
+  padding-top: 0px;
 }
 
 .production-brief-table {

+ 21 - 13
src/views/pms/qhse/ptw/index.vue

@@ -98,13 +98,14 @@
 
           <zm-table-column label="附件" align="center" min-width="90">
             <template #default="{ row }">
-              <el-link
+              <el-button
+                text
                 v-if="row.file"
                 :underline="false"
                 type="primary"
                 size="small"
                 @click="viewFile(row.file)"
-                >查看</el-link
+                >查看</el-button
               >
               <span v-else>-</span>
             </template>
@@ -303,25 +304,25 @@
   </Dialog>
 
   <!-- 文件查看对话框 -->
-  <Dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <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>
+      class="mt-5 flex min-w-0 items-center justify-between gap-5">
+      <span class="file-name-text flex-1 min-w-0">{{ extractFileName(file) }}</span>
+      <div class="flex shrink-0 items-center gap-5 flex-nowrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
-          <Icon icon="ep:view" class="mr-2px" />查看</el-button
-        >
+          <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
-        >
+          <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>
+        <el-button type="primary" @click="dialogFileView = false">确认</el-button>
       </div>
     </template>
   </Dialog>
@@ -485,13 +486,14 @@
 
         <el-table-column label="附件" align="center" prop="jsaFile">
           <template #default="{ row }">
-            <el-link
+            <el-button
+              text
               v-if="row.jsaFile"
               :underline="false"
               type="primary"
               size="small"
               @click="viewFile(row.jsaFile)"
-              >查看</el-link
+              >查看</el-button
             >
             <span v-else>-</span>
           </template>
@@ -1025,6 +1027,12 @@ const handleRadioChange2 = (row: any) => {
 </script>
 
 <style scoped>
+.file-name-text {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
 ::deep(.el-tree--highlight-current) {
   height: 200px !important;
 }

+ 44 - 10
src/views/pms/qhse/safety/index.vue

@@ -148,13 +148,14 @@
 
           <zm-table-column label="附件" align="center" prop="hazardFile" min-width="90">
             <template #default="{ row }">
-              <el-link
+              <el-button
+                text
                 v-if="row.hazardFile"
                 :underline="false"
                 type="primary"
                 size="small"
                 @click="viewFile(row.hazardFile)"
-                >查看</el-link
+                >查看</el-button
               >
               <span v-else>-</span>
             </template>
@@ -166,13 +167,14 @@
 
           <zm-table-column label="整改附件" align="center" prop="rectifyFile" min-width="90">
             <template #default="{ row }">
-              <el-link
+              <el-button
+                text
                 v-if="row.rectifyFile"
                 :underline="false"
                 type="primary"
                 size="small"
                 @click="viewFile(row.rectifyFile)"
-                >查看</el-link
+                >查看</el-button
               >
               <span v-else>-</span>
             </template>
@@ -355,7 +357,7 @@
   </Dialog>
 
   <!-- 整改 -->
-  <el-dialog
+  <Dialog
     title="整改"
     v-model="fixdDialogVisible"
     width="600px"
@@ -389,15 +391,15 @@
       <el-button @click="closeDialog2">取 消</el-button>
       <el-button type="primary" @click="submitFix" :loading="submitLoading">确 定</el-button>
     </template>
-  </el-dialog>
+  </Dialog>
 
-  <el-dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件">
     <div
       v-for="(file, index) in fileList"
       :key="index"
-      class="flex items-center justify-between mt-5">
+      class="flex items-center justify-between mt-5 gap-5">
       <span class="file-name-text">{{ extractFileName(file) }}</span>
-      <div>
+      <div class="flex gap-5 no-wrap">
         <el-button link type="primary" @click="viewFileInfo(file)">
           <Icon icon="ep:view" class="mr-2px" />查看</el-button
         >
@@ -412,7 +414,7 @@
         <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
       </div>
     </template>
-  </el-dialog>
+  </Dialog>
 
   <FilePreviewDialog
     v-model="filePreviewVisible"
@@ -530,6 +532,32 @@ const getList = async () => {
   }
 }
 
+const downloadFile = (response: any) => {
+  const blob = new Blob([response], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
+  })
+
+  let fileName = '排查治理记录.xlsx'
+  const disposition = response.headers ? response.headers['content-disposition'] : ''
+  if (disposition) {
+    const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
+    const matches = filenameRegex.exec(disposition)
+    if (matches != null && matches[1]) {
+      fileName = matches[1].replace(/['"]/g, '')
+    }
+  }
+
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.setAttribute('download', fileName)
+
+  document.body.appendChild(link)
+  link.click()
+
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
 const handleExport = async () => {
   try {
     exportLoading.value = true
@@ -1103,4 +1131,10 @@ watch(
     min-height: 220px;
   }
 }
+
+.file-name-text {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
 </style>

+ 661 - 313
src/views/pms/stat/inspect.vue

@@ -1,254 +1,260 @@
 <template>
-  <!-- 第一行:筛选条件 -->
-  <el-row :gutter="16" class="mb-6">
-    <el-col :span="24">
-      <el-card class="filter-card" shadow="hover" style="border: none">
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="80px"
-        >
-          <el-form-item label="所属部门" prop="project_name">
-            <el-tree-select
-              v-model="queryParams.deptId"
-              :data="deptList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              filterable
-              placeholder="请选择所在部门"
-              clearable
-              style="width: 200px"
-            />
-          </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-240px"
-              @change="handleDateChange"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="handleQuery">
-              <Icon icon="ep:search" class="mr-5px" /> 搜索
-            </el-button>
-            <el-button @click="resetQuery" class="btn-default">
-              <Icon icon="ep:refresh" class="mr-5px" /> 重置
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第二行:统计卡片行 -->
-  <el-row :gutter="16" class="mb-6">
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-neutral-1" shadow="hover" style="border: none">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:calendar" class="stat-icon" />
-            </div>
-            <div class="stat-title">昨日工单数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">总数量</div>
-              <div class="stat-sub-value text-slate-700">{{ day.total }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">未完成</div>
-              <div class="stat-sub-value text-amber-600">{{ day.todo }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-neutral-2" shadow="hover" style="border: none">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:star" class="stat-icon" />
-            </div>
-            <div class="stat-title">近一周工单数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">总数量</div>
-              <div class="stat-sub-value text-slate-700">{{ week.total }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">未完成</div>
-              <div class="stat-sub-value text-amber-600">{{ week.todo }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-neutral-3" shadow="hover" style="border: none">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:trend-charts" class="stat-icon" />
-            </div>
-            <div class="stat-title">近一月工单数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">总数量</div>
-              <div class="stat-sub-value text-slate-700">{{ month.total }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">未完成</div>
-              <div class="stat-sub-value text-amber-600">{{ month.todo }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-neutral-4" shadow="hover" style="border: none">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:data-analysis" class="stat-icon" />
-            </div>
-            <div class="stat-title">工单总数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">总数量</div>
-              <div class="stat-sub-value text-slate-700">{{ total.total }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">未完成</div>
-              <div class="stat-sub-value text-amber-600">{{ total.todo }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第三行:图表行 -->
-  <el-row :gutter="16" class="mb-6">
-    <el-col :span="12">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot chart-title-dot-blue"></div>
-              <span class="chart-title">工单状态统计</span>
-            </div>
-          </div>
-        </template>
-        <el-row class="chart-grid">
-          <el-col :span="8" class="chart-item">
-            <div ref="finishedChartRef" class="gauge-chart"></div>
-            <div class="chart-label clickable" @click="clickStatus('工单已执行')">
-              <span class="label-dot label-dot-blue"></span>
-              <span class="label-text">已执行</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="chart-item">
-            <div ref="writeChartRef" class="gauge-chart"></div>
-            <div class="chart-label clickable" @click="clickStatus('工单待执行')">
-              <span class="label-dot label-dot-amber"></span>
-              <span class="label-text">待执行</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="chart-item">
-            <div ref="ignoreChartRef" class="gauge-chart"></div>
-            <div class="chart-label clickable" @click="clickStatus('工单忽略')">
-              <span class="label-dot label-dot-gray"></span>
-              <span class="label-text">忽略</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-    <el-col :span="12">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot chart-title-dot-green"></div>
-              <span class="chart-title">设备状态统计</span>
-            </div>
-          </div>
-        </template>
-        <el-row class="chart-grid">
-          <el-col :span="8" class="chart-item">
-            <div ref="finishedTodayChartRef" class="gauge-chart"></div>
-            <div class="chart-label clickable" @click="clickStatus('设备已执行')">
-              <span class="label-dot label-dot-blue"></span>
-              <span class="label-text">已填写</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="chart-item">
-            <div ref="writeTodayChartRef" class="gauge-chart"></div>
-            <div class="chart-label clickable" @click="clickStatus('设备待执行')">
-              <span class="label-dot label-dot-amber"></span>
-              <span class="label-text">未填写</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="chart-item">
-            <div ref="ignoreTodayChartRef" class="gauge-chart"></div>
-            <div class="chart-label clickable" @click="clickStatus('设备忽略')">
-              <span class="label-dot label-dot-gray"></span>
-              <span class="label-text">忽略</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第四行:消息统计行 -->
-  <el-row :gutter="16" class="mb-6">
-    <el-col :span="8">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot chart-title-dot-purple"></div>
-              <span class="chart-title">巡检项状态统计</span>
-            </div>
-          </div>
-        </template>
-        <div ref="statusChartRef" class="pie-chart-container"></div>
-      </el-card>
-    </el-col>
-    <el-col :span="16">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot chart-title-dot-indigo"></div>
-              <span class="chart-title">近一年数量统计</span>
-            </div>
-          </div>
-        </template>
-        <div ref="chartContainer" class="bar-chart-container"></div>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- TODO 第五行:地图 -->
+  <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div class="bg kb-screen" id="inspect-kb" :style="targetAreaStyle">
+        <header class="header">巡检统计</header>
+        <main class="inspect-kb-content">
+          <!-- 第一行:筛选条件 -->
+          <el-row :gutter="16" class="filter-row">
+            <el-col :span="24">
+              <el-card class="filter-card" shadow="hover" style="border: none">
+                <el-form
+                  class="-mb-15px"
+                  :model="queryParams"
+                  ref="queryFormRef"
+                  :inline="true"
+                  label-width="80px">
+                  <el-form-item label="所属部门" prop="project_name">
+                    <el-tree-select
+                      v-model="queryParams.deptId"
+                      :data="deptList"
+                      :props="defaultProps"
+                      check-strictly
+                      node-key="id"
+                      filterable
+                      placeholder="请选择所在部门"
+                      clearable
+                      style="width: 200px" />
+                  </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-240px"
+                      @change="handleDateChange" />
+                  </el-form-item>
+                  <el-form-item>
+                    <el-button type="primary" @click="handleQuery">
+                      <Icon icon="ep:search" class="mr-5px" /> 搜索
+                    </el-button>
+                    <el-button @click="resetQuery" class="btn-default">
+                      <Icon icon="ep:refresh" class="mr-5px" /> 重置
+                    </el-button>
+                  </el-form-item>
+                </el-form>
+              </el-card>
+            </el-col>
+          </el-row>
+
+          <!-- 第二行:统计卡片行 -->
+          <el-row :gutter="16" class="summary-row">
+            <el-col :span="6">
+              <el-card class="stat-card stat-card-neutral-1" shadow="hover" style="border: none">
+                <div class="stat-content">
+                  <div class="stat-header">
+                    <div class="stat-icon-wrapper">
+                      <Icon icon="ep:calendar" class="stat-icon" />
+                    </div>
+                    <div class="stat-title">昨日工单数量</div>
+                  </div>
+                  <el-divider class="stat-divider" />
+                  <div class="stat-body">
+                    <div class="stat-item">
+                      <div class="stat-sub-label">总数量</div>
+                      <div class="stat-sub-value text-slate-700">{{ day.total }}</div>
+                    </div>
+                    <div class="stat-item">
+                      <div class="stat-sub-label">未完成</div>
+                      <div class="stat-sub-value text-amber-600">{{ day.todo }}</div>
+                    </div>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="6">
+              <el-card class="stat-card stat-card-neutral-2" shadow="hover" style="border: none">
+                <div class="stat-content">
+                  <div class="stat-header">
+                    <div class="stat-icon-wrapper">
+                      <Icon icon="ep:star" class="stat-icon" />
+                    </div>
+                    <div class="stat-title">近一周工单数量</div>
+                  </div>
+                  <el-divider class="stat-divider" />
+                  <div class="stat-body">
+                    <div class="stat-item">
+                      <div class="stat-sub-label">总数量</div>
+                      <div class="stat-sub-value text-slate-700">{{ week.total }}</div>
+                    </div>
+                    <div class="stat-item">
+                      <div class="stat-sub-label">未完成</div>
+                      <div class="stat-sub-value text-amber-600">{{ week.todo }}</div>
+                    </div>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="6">
+              <el-card class="stat-card stat-card-neutral-3" shadow="hover" style="border: none">
+                <div class="stat-content">
+                  <div class="stat-header">
+                    <div class="stat-icon-wrapper">
+                      <Icon icon="ep:trend-charts" class="stat-icon" />
+                    </div>
+                    <div class="stat-title">近一月工单数量</div>
+                  </div>
+                  <el-divider class="stat-divider" />
+                  <div class="stat-body">
+                    <div class="stat-item">
+                      <div class="stat-sub-label">总数量</div>
+                      <div class="stat-sub-value text-slate-700">{{ month.total }}</div>
+                    </div>
+                    <div class="stat-item">
+                      <div class="stat-sub-label">未完成</div>
+                      <div class="stat-sub-value text-amber-600">{{ month.todo }}</div>
+                    </div>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="6">
+              <el-card class="stat-card stat-card-neutral-4" shadow="hover" style="border: none">
+                <div class="stat-content">
+                  <div class="stat-header">
+                    <div class="stat-icon-wrapper">
+                      <Icon icon="ep:data-analysis" class="stat-icon" />
+                    </div>
+                    <div class="stat-title">工单总数量</div>
+                  </div>
+                  <el-divider class="stat-divider" />
+                  <div class="stat-body">
+                    <div class="stat-item">
+                      <div class="stat-sub-label">总数量</div>
+                      <div class="stat-sub-value text-slate-700">{{ total.total }}</div>
+                    </div>
+                    <div class="stat-item">
+                      <div class="stat-sub-label">未完成</div>
+                      <div class="stat-sub-value text-amber-600">{{ total.todo }}</div>
+                    </div>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+
+          <!-- 第三行:图表行 -->
+          <el-row :gutter="16" class="status-row">
+            <el-col :span="12">
+              <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
+                <template #header>
+                  <div class="chart-header">
+                    <div class="chart-title-wrapper">
+                      <div class="chart-title-dot chart-title-dot-blue"></div>
+                      <span class="chart-title">工单状态统计</span>
+                    </div>
+                  </div>
+                </template>
+                <el-row class="chart-grid">
+                  <el-col :span="8" class="chart-item">
+                    <div ref="finishedChartRef" class="gauge-chart"></div>
+                    <div class="chart-label clickable" @click="clickStatus('工单已执行')">
+                      <span class="label-dot label-dot-blue"></span>
+                      <span class="label-text">已执行</span>
+                    </div>
+                  </el-col>
+                  <el-col :span="8" class="chart-item">
+                    <div ref="writeChartRef" class="gauge-chart"></div>
+                    <div class="chart-label clickable" @click="clickStatus('工单待执行')">
+                      <span class="label-dot label-dot-amber"></span>
+                      <span class="label-text">待执行</span>
+                    </div>
+                  </el-col>
+                  <el-col :span="8" class="chart-item">
+                    <div ref="ignoreChartRef" class="gauge-chart"></div>
+                    <div class="chart-label clickable" @click="clickStatus('工单忽略')">
+                      <span class="label-dot label-dot-gray"></span>
+                      <span class="label-text">忽略</span>
+                    </div>
+                  </el-col>
+                </el-row>
+              </el-card>
+            </el-col>
+            <el-col :span="12">
+              <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
+                <template #header>
+                  <div class="chart-header">
+                    <div class="chart-title-wrapper">
+                      <div class="chart-title-dot chart-title-dot-green"></div>
+                      <span class="chart-title">设备状态统计</span>
+                    </div>
+                  </div>
+                </template>
+                <el-row class="chart-grid">
+                  <el-col :span="8" class="chart-item">
+                    <div ref="finishedTodayChartRef" class="gauge-chart"></div>
+                    <div class="chart-label clickable" @click="clickStatus('设备已执行')">
+                      <span class="label-dot label-dot-blue"></span>
+                      <span class="label-text">已填写</span>
+                    </div>
+                  </el-col>
+                  <el-col :span="8" class="chart-item">
+                    <div ref="writeTodayChartRef" class="gauge-chart"></div>
+                    <div class="chart-label clickable" @click="clickStatus('设备待执行')">
+                      <span class="label-dot label-dot-amber"></span>
+                      <span class="label-text">未填写</span>
+                    </div>
+                  </el-col>
+                  <el-col :span="8" class="chart-item">
+                    <div ref="ignoreTodayChartRef" class="gauge-chart"></div>
+                    <div class="chart-label clickable" @click="clickStatus('设备忽略')">
+                      <span class="label-dot label-dot-gray"></span>
+                      <span class="label-text">忽略</span>
+                    </div>
+                  </el-col>
+                </el-row>
+              </el-card>
+            </el-col>
+          </el-row>
+
+          <!-- 第四行:消息统计行 -->
+          <el-row :gutter="16" class="trend-row">
+            <el-col :span="8">
+              <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
+                <template #header>
+                  <div class="chart-header">
+                    <div class="chart-title-wrapper">
+                      <div class="chart-title-dot chart-title-dot-purple"></div>
+                      <span class="chart-title">巡检项状态统计</span>
+                    </div>
+                  </div>
+                </template>
+                <div ref="statusChartRef" class="pie-chart-container"></div>
+              </el-card>
+            </el-col>
+            <el-col :span="16">
+              <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
+                <template #header>
+                  <div class="chart-header">
+                    <div class="chart-title-wrapper">
+                      <div class="chart-title-dot chart-title-dot-indigo"></div>
+                      <span class="chart-title">近一年数量统计</span>
+                    </div>
+                  </div>
+                </template>
+                <div ref="chartContainer" class="bar-chart-container"></div>
+              </el-card>
+            </el-col>
+          </el-row>
+
+          <!-- TODO 第五行:地图 -->
+        </main>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts" name="Index">
@@ -277,12 +283,56 @@ import { ref } from 'vue'
 import { DeptTreeItem } from '@/api/system/dept'
 import * as DeptApi from '@/api/system/dept'
 import { useUserStore } from '@/store/modules/user'
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
 
 // TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
 
 /** IoT 首页 */
 defineOptions({ name: 'IotInspectStat' })
 
+const wrapperRef = ref<HTMLDivElement>()
+const scale = ref(1)
+const chartInstances = new Set<ReturnType<typeof echarts.init>>()
+
+let resizeObserver: ResizeObserver | null = null
+let resizeRaf = 0
+
+const targetWrapperStyle = computed(() => ({
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const targetAreaStyle = computed(() => ({
+  '--kb-scale': scale.value,
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const getChartInstance = (el: HTMLElement | null) => {
+  if (!el) return null
+  const chart = echarts.getInstanceByDom(el) || echarts.init(el)
+  chartInstances.add(chart)
+  return chart
+}
+
+const resizeCharts = () => {
+  chartInstances.forEach((chart) => chart.resize())
+}
+
+const updateScale = () => {
+  cancelAnimationFrame(resizeRaf)
+  resizeRaf = requestAnimationFrame(() => {
+    const wrapper = wrapperRef.value
+    if (!wrapper) return
+
+    const { clientWidth, clientHeight } = wrapper
+    if (!clientWidth || !clientHeight) return
+
+    scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(resizeCharts)
+  })
+}
+
 // TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
 echarts.use([
   TooltipComponent,
@@ -505,7 +555,8 @@ const getStats = async () => {
 
 /** 初始化图表 */
 const initCharts = () => {
-  const chart = echarts.init(statusChartRef.value)
+  const chart = getChartInstance(statusChartRef.value)
+  if (!chart) return
   chart.setOption({
     tooltip: {
       trigger: 'item'
@@ -543,6 +594,7 @@ const initCharts = () => {
       }
     ]
   })
+  chart.off('click')
   chart.on('click', (params) => {
     console.log('点击的数据值为:', params.value)
     console.log('点击的数据类型为:', params.data.type)
@@ -601,7 +653,8 @@ const initCharts = () => {
 
 /** 初始化仪表盘图表 */
 const initGaugeChart = (el: any, value: number, color: string, type: string) => {
-  const chart = echarts.init(el)
+  const chart = getChartInstance(el)
+  if (!chart) return
   chart.setOption({
     series: [
       {
@@ -646,6 +699,7 @@ const initGaugeChart = (el: any, value: number, color: string, type: string) =>
   })
 
   // 添加点击事件监听器
+  chart.off('click')
   chart.on('click', (params) => {
     console.log('点击的数据值为:', params.value)
     console.log('点击的数据类型为:', params.data.type)
@@ -701,7 +755,7 @@ const initMessageChart = () => {
   })
 
   // 配置图表
-  echarts.init(deviceMessageCountChartRef.value).setOption({
+  getChartInstance(deviceMessageCountChartRef.value)?.setOption({
     tooltip: {
       trigger: 'axis',
       backgroundColor: 'rgba(255, 255, 255, 0.9)',
@@ -794,7 +848,7 @@ const initMessageChart = () => {
 }
 
 const chartContainer = ref(null)
-let chartInstance = null
+let chartInstance: ReturnType<typeof echarts.init> | null = null
 
 // 生成过去12个月份的标签 (格式: YYYY-MM)
 const generateMonthLabels = () => {
@@ -885,11 +939,9 @@ const initChart = async () => {
   }
 
   // 初始化图表
-  chartInstance = echarts.init(chartContainer.value)
+  chartInstance = getChartInstance(chartContainer.value)
+  if (!chartInstance) return
   chartInstance.setOption(option)
-
-  // 窗口缩放监听
-  window.addEventListener('resize', handleResize)
   handleResize()
 }
 
@@ -899,28 +951,62 @@ const handleResize = () => {
 }
 /** 初始化 */
 onMounted(async () => {
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  window.addEventListener('resize', updateScale)
+
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
   queryParams.deptId = useUserStore().getUser.deptId
   await getStats()
+  nextTick(resizeCharts)
   // await initChart()
   // await initCharts()
 })
 onUnmounted(() => {
-  chartInstance?.dispose()
-  window.removeEventListener('resize', handleResize)
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  cancelAnimationFrame(resizeRaf)
+  chartInstances.forEach((chart) => chart.dispose())
+  chartInstances.clear()
 })
 </script>
 
 <style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+// 响应式优化
+@media (width <= 1200px) {
+  .stat-sub-value {
+    font-size: 24px;
+  }
+}
+
+@media (width <= 768px) {
+  .stat-card {
+    margin-bottom: 16px;
+  }
+
+  .chart-item {
+    padding: 8px;
+  }
+
+  .gauge-chart {
+    height: 140px;
+  }
+}
+
 // 筛选卡片样式
 .filter-card {
-  border-radius: 12px;
+  background: linear-gradient(to bottom, #fafafa, #fff);
   border: 1px solid #e5e7eb;
-  background: linear-gradient(to bottom, #fafafa, #ffffff);
+  border-radius: 12px;
   transition: all 0.3s ease;
 
   &:hover {
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+    box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
   }
 
   :deep(.el-card__body) {
@@ -928,8 +1014,8 @@ onUnmounted(() => {
   }
 
   :deep(.el-form-item__label) {
-    color: #6b7280;
     font-weight: 500;
+    color: #6b7280;
   }
 }
 
@@ -941,35 +1027,35 @@ onUnmounted(() => {
   &:hover {
     background: linear-gradient(135deg, #475569 0%, #334155 100%);
     transform: translateY(-1px);
-    box-shadow: 0 4px 8px rgba(100, 116, 139, 0.3);
+    box-shadow: 0 4px 8px rgb(100 116 139 / 30%);
   }
 }
 
 .btn-default {
-  border-color: #d1d5db;
   color: #6b7280;
+  border-color: #d1d5db;
   transition: all 0.3s ease;
 
   &:hover {
-    border-color: #9ca3af;
     color: #4b5563;
     background: #f9fafb;
+    border-color: #9ca3af;
   }
 }
 
 // 统计卡片基础样式
 .stat-card {
-  border-radius: 12px;
+  position: relative;
+  overflow: hidden;
+  background: #fff;
   border: 1px solid #e5e7eb;
+  border-radius: 12px;
   transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-  overflow: hidden;
-  position: relative;
-  background: #ffffff;
 
   &:hover {
-    transform: translateY(-4px);
-    box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.1);
     border-color: #d1d5db;
+    transform: translateY(-4px);
+    box-shadow: 0 12px 24px -8px rgb(0 0 0 / 10%);
   }
 
   :deep(.el-card__body) {
@@ -1007,14 +1093,14 @@ onUnmounted(() => {
 }
 
 .stat-icon-wrapper {
+  display: flex;
   width: 48px;
   height: 48px;
   background: #0090ff;
   border-radius: 12px;
-  display: flex;
+  box-shadow: 0 4px 8px rgb(100 116 139 / 20%);
   align-items: center;
   justify-content: center;
-  box-shadow: 0 4px 8px rgba(100, 116, 139, 0.2);
 }
 
 .stat-icon {
@@ -1029,8 +1115,8 @@ onUnmounted(() => {
 }
 
 .stat-divider {
-  border-color: #e2e8f0;
   margin: 12px 0;
+  border-color: #e2e8f0;
 }
 
 .stat-body {
@@ -1044,19 +1130,19 @@ onUnmounted(() => {
   justify-content: space-between;
   align-items: center;
   padding: 8px 12px;
-  background: rgba(255, 255, 255, 0.6);
+  background: rgb(255 255 255 / 60%);
   border-radius: 8px;
   transition: all 0.2s ease;
 
   &:hover {
-    background: rgba(255, 255, 255, 0.9);
+    background: rgb(255 255 255 / 90%);
   }
 }
 
 .stat-sub-label {
   font-size: 13px;
-  color: #64748b;
   font-weight: 500;
+  color: #64748b;
 }
 
 .stat-sub-value {
@@ -1067,20 +1153,20 @@ onUnmounted(() => {
 
 // 图表卡片增强样式
 .chart-card-enhanced {
-  border-radius: 12px;
+  background: #fff;
   border: 1px solid #e5e7eb;
+  border-radius: 12px;
   transition: all 0.3s ease;
-  background: #ffffff;
 
   &:hover {
-    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
     border-color: #d1d5db;
+    box-shadow: 0 8px 24px rgb(0 0 0 / 8%);
   }
 
   :deep(.el-card__header) {
     padding: 16px 20px;
+    background: linear-gradient(to right, #fafafa, #fff);
     border-bottom: 1px solid #f1f5f9;
-    background: linear-gradient(to right, #fafafa, #ffffff);
   }
 
   :deep(.el-card__body) {
@@ -1104,30 +1190,30 @@ onUnmounted(() => {
   width: 8px;
   height: 8px;
   border-radius: 50%;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
 }
 
 .chart-title-dot-blue {
-  background: #0055bb;
+  background: #05b;
 }
 
 .chart-title-dot-green {
-  background: #0055bb;
+  background: #05b;
 }
 
 .chart-title-dot-purple {
-  background: #0055bb;
+  background: #05b;
 }
 
 .chart-title-dot-indigo {
-  background: #0055bb;
+  background: #05b;
 }
 
 .chart-title {
   font-size: 16px;
   font-weight: 600;
-  color: #334155;
   letter-spacing: 0.3px;
+  color: #334155;
 }
 
 .chart-grid {
@@ -1154,13 +1240,13 @@ onUnmounted(() => {
 
 .chart-label {
   display: flex;
-  align-items: center;
-  gap: 6px;
-  margin-top: 8px;
   padding: 6px 12px;
+  margin-top: 8px;
   background: #f1f5f9;
   border-radius: 20px;
   transition: all 0.2s ease;
+  align-items: center;
+  gap: 6px;
 
   .chart-item:hover & {
     background: #e2e8f0;
@@ -1177,61 +1263,323 @@ onUnmounted(() => {
 }
 
 .label-dot {
+  display: inline-block;
   width: 8px;
   height: 8px;
   border-radius: 50%;
-  display: inline-block;
 }
 
 .label-dot-blue {
   background: #64748b;
-  box-shadow: 0 0 0 2px rgba(100, 116, 139, 0.2);
+  box-shadow: 0 0 0 2px rgb(100 116 139 / 20%);
 }
 
 .label-dot-amber {
   background: #d97706;
-  box-shadow: 0 0 0 2px rgba(217, 119, 6, 0.2);
+  box-shadow: 0 0 0 2px rgb(217 119 6 / 20%);
 }
 
 .label-dot-gray {
   background: #78716c;
-  box-shadow: 0 0 0 2px rgba(120, 113, 108, 0.2);
+  box-shadow: 0 0 0 2px rgb(120 113 108 / 20%);
 }
 
 .label-text {
   font-size: 13px;
-  color: #64748b;
   font-weight: 500;
+  color: #64748b;
 }
 
 .pie-chart-container {
-  height: 300px;
   width: 100%;
+  height: 300px;
 }
 
 .bar-chart-container {
+  width: 100%;
   height: 300px;
+}
+
+.kb-screen {
+  overflow: hidden;
+}
+
+.inspect-kb-content {
+  display: grid;
+  height: calc(100% - 52px * var(--kb-scale));
+  padding: calc(16px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
+  gap: calc(12px * var(--kb-scale));
+  grid-template-rows:
+    calc(74px * var(--kb-scale)) calc(174px * var(--kb-scale))
+    calc(300px * var(--kb-scale)) minmax(0, 1fr);
+}
+
+.inspect-kb-content > .el-row {
+  height: 100%;
+  margin-bottom: 0 !important;
+}
+
+.inspect-kb-content > .el-row > .el-col {
+  height: 100%;
+}
+
+.filter-card,
+.stat-card,
+.chart-card-enhanced {
+  height: 100%;
+  background: linear-gradient(180deg, rgb(235 243 255 / 76%) 0%, rgb(221 233 251 / 58%) 100%);
+  border: 1px solid rgb(255 255 255 / 68%) !important;
+  border-radius: calc(8px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(10px * var(--kb-scale)) calc(24px * var(--kb-scale)) rgb(46 90 164 / 10%);
+  backdrop-filter: blur(12px);
+}
+
+.filter-card:hover,
+.stat-card:hover,
+.chart-card-enhanced:hover {
+  border-color: rgb(255 255 255 / 84%) !important;
+  transform: none;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 86%),
+    0 calc(12px * var(--kb-scale)) calc(26px * var(--kb-scale)) rgb(46 90 164 / 14%);
+}
+
+.filter-card :deep(.el-card__body) {
+  display: flex;
+  height: 100%;
+  padding: calc(12px * var(--kb-scale)) calc(18px * var(--kb-scale));
+  align-items: center;
+}
+
+.filter-card :deep(.el-form) {
+  display: flex;
   width: 100%;
+  margin-bottom: 0 !important;
+  align-items: center;
+  gap: calc(16px * var(--kb-scale));
 }
 
-// 响应式优化
-@media (max-width: 1200px) {
-  .stat-sub-value {
-    font-size: 24px;
-  }
+.filter-card :deep(.el-form-item) {
+  margin-right: 0;
+  margin-bottom: 0;
 }
 
-@media (max-width: 768px) {
-  .stat-card {
-    margin-bottom: 16px;
-  }
+.filter-card :deep(.el-form-item__label) {
+  height: calc(32px * var(--kb-scale));
+  padding-right: calc(10px * var(--kb-scale));
+  font-size: calc(14px * var(--kb-scale));
+  line-height: calc(32px * var(--kb-scale));
+  color: #31527e;
+}
 
-  .chart-item {
-    padding: 8px;
-  }
+.filter-card :deep(.el-tree-select) {
+  width: calc(220px * var(--kb-scale)) !important;
+}
 
-  .gauge-chart {
-    height: 140px;
-  }
+.filter-card :deep(.el-date-editor) {
+  width: calc(320px * var(--kb-scale)) !important;
+}
+
+.filter-card :deep(.el-input__wrapper),
+.filter-card :deep(.el-select__wrapper) {
+  min-height: calc(32px * var(--kb-scale));
+  padding: 0 calc(10px * var(--kb-scale));
+  background: rgb(255 255 255 / 72%);
+}
+
+.filter-card :deep(.el-range-input),
+.filter-card :deep(.el-range-separator),
+.filter-card :deep(.el-input__inner) {
+  font-size: calc(13px * var(--kb-scale));
+}
+
+.filter-card :deep(.el-button) {
+  height: calc(32px * var(--kb-scale));
+  padding: 0 calc(14px * var(--kb-scale));
+  margin-left: calc(8px * var(--kb-scale));
+  font-size: calc(13px * var(--kb-scale));
+}
+
+.stat-card {
+  overflow: hidden;
+}
+
+.stat-card :deep(.el-card__body) {
+  height: 100%;
+  padding: 0;
+}
+
+.stat-card-neutral-1,
+.stat-card-neutral-2,
+.stat-card-neutral-3,
+.stat-card-neutral-4 {
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 42%) 100%);
+}
+
+.stat-content {
+  display: flex;
+  height: 100%;
+  padding: calc(16px * var(--kb-scale));
+  flex-direction: column;
+}
+
+.stat-header {
+  margin-bottom: calc(8px * var(--kb-scale));
+  gap: calc(10px * var(--kb-scale));
+}
+
+.stat-icon-wrapper {
+  width: calc(38px * var(--kb-scale));
+  height: calc(38px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(235 244 255 / 92%) 100%);
+  border: 1px solid rgb(255 255 255 / 88%);
+  border-radius: calc(8px * var(--kb-scale));
+  box-shadow: 0 calc(5px * var(--kb-scale)) calc(10px * var(--kb-scale)) rgb(57 111 194 / 12%);
+}
+
+.stat-icon {
+  font-size: calc(19px * var(--kb-scale));
+  color: #2474dd;
+}
+
+.stat-title {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(20px * var(--kb-scale));
+  font-weight: normal;
+  letter-spacing: 0;
+  color: #03409b;
+}
+
+.stat-divider {
+  margin: 0 0 calc(8px * var(--kb-scale));
+  border-color: rgb(3 64 155 / 12%);
+}
+
+.stat-body {
+  display: grid;
+  min-height: 0;
+  flex: 1;
+  gap: calc(10px * var(--kb-scale));
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.stat-item {
+  padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale));
+  background: rgb(255 255 255 / 38%);
+  border: 1px solid rgb(255 255 255 / 46%);
+  border-radius: calc(6px * var(--kb-scale));
+}
+
+.stat-sub-label {
+  font-size: calc(13px * var(--kb-scale));
+  color: #55708f;
+}
+
+.stat-sub-value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(28px * var(--kb-scale));
+  font-weight: normal;
+  color: #1762c8;
+}
+
+.stat-sub-value.text-amber-600 {
+  color: #e87b1e;
+}
+
+.chart-card-enhanced {
+  display: flex;
+  overflow: hidden;
+  flex-direction: column;
+}
+
+.chart-card-enhanced :deep(.el-card__header) {
+  height: calc(42px * var(--kb-scale));
+  padding: 0 calc(16px * var(--kb-scale)) 0 calc(36px * var(--kb-scale));
+  background: transparent;
+  border-bottom: 1px solid rgb(3 64 155 / 12%);
+}
+
+.chart-card-enhanced :deep(.el-card__body) {
+  min-height: 0;
+  padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale));
+  flex: 1;
+}
+
+.chart-header,
+.chart-title-wrapper {
+  height: 100%;
+}
+
+.chart-title-wrapper {
+  position: relative;
+}
+
+.chart-title-dot {
+  position: absolute;
+  left: calc(-22px * var(--kb-scale));
+  width: calc(4px * var(--kb-scale));
+  height: calc(18px * var(--kb-scale));
+  background: #03409b;
+  border-radius: calc(2px * var(--kb-scale));
+  box-shadow: none;
+}
+
+.chart-title {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(22px * var(--kb-scale));
+  font-weight: normal;
+  letter-spacing: 0;
+  color: #03409b;
+}
+
+.chart-grid {
+  height: 100%;
+  min-height: 0;
+}
+
+.chart-item {
+  height: 100%;
+  padding: calc(4px * var(--kb-scale));
+  justify-content: center;
+}
+
+.chart-item:hover {
+  background: transparent;
+}
+
+.gauge-chart {
+  height: calc(190px * var(--kb-scale));
+}
+
+.chart-label {
+  min-height: calc(28px * var(--kb-scale));
+  padding: calc(4px * var(--kb-scale)) calc(10px * var(--kb-scale));
+  margin-top: 0;
+  background: rgb(255 255 255 / 52%);
+  border-radius: calc(5px * var(--kb-scale));
+  gap: calc(6px * var(--kb-scale));
+}
+
+.chart-label.clickable:hover {
+  background: rgb(255 255 255 / 78%);
+  transform: none;
+}
+
+.label-dot {
+  width: calc(7px * var(--kb-scale));
+  height: calc(7px * var(--kb-scale));
+}
+
+.label-text {
+  font-size: calc(13px * var(--kb-scale));
+  color: #466484;
+}
+
+.pie-chart-container,
+.bar-chart-container {
+  width: 100%;
+  height: 100%;
 }
 </style>

+ 540 - 287
src/views/pms/stat/maintain.vue

@@ -1,186 +1,156 @@
 <template>
-  <!-- 第一行:统计卡片行 -->
-  <el-row :gutter="16" class="mb-6">
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-gradient-1" shadow="hover">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:data-line" class="stat-icon" />
-            </div>
-            <div class="stat-title">MTTR</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-label">平均解决时间</div>
-            <div class="stat-value">{{ statsData.productCategoryCount }}</div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-gradient-2" shadow="hover">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:trend-charts" class="stat-icon" />
-            </div>
-            <div class="stat-title">近一周工单数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">故障上报</div>
-              <div class="stat-sub-value text-blue-500">{{ week.failureWeek }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">维修工单</div>
-              <div class="stat-sub-value text-white">{{ week.maintainWeek }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-gradient-3" shadow="hover">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:calendar" class="stat-icon" />
-            </div>
-            <div class="stat-title">近一月工单数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">故障上报</div>
-              <div class="stat-sub-value text-blue-500">{{ month.failureMonth }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">维修工单</div>
-              <div class="stat-sub-value text-white">{{ month.maintainMonth }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card stat-card-gradient-4" shadow="hover">
-        <div class="stat-content">
-          <div class="stat-header">
-            <div class="stat-icon-wrapper">
-              <Icon icon="ep:pie-chart" class="stat-icon" />
-            </div>
-            <div class="stat-title">工单总数量</div>
-          </div>
-          <el-divider class="stat-divider" />
-          <div class="stat-body">
-            <div class="stat-item">
-              <div class="stat-sub-label">故障上报</div>
-              <div class="stat-sub-value text-blue-500">{{ total.failureTotal }}</div>
-            </div>
-            <div class="stat-item">
-              <div class="stat-sub-label">维修工单</div>
-              <div class="stat-sub-value text-white">{{ total.maintainTotal }}</div>
-            </div>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第二行:图表行 -->
-  <el-row :gutter="16" class="mb-6">
-    <el-col :span="12">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot"></div>
-              <span class="chart-title">故障上报状态统计</span>
-            </div>
-          </div>
-        </template>
-        <el-row class="chart-grid">
-          <el-col :span="6" class="chart-item">
-            <div ref="reportingChartRef" class="gauge-chart"></div>
-            <div class="chart-label">
-              <span class="label-dot label-dot-blue"></span>
-              <span class="label-text">上报中</span>
-            </div>
-          </el-col>
-          <el-col :span="6" class="chart-item">
-            <div ref="dealFinishedChartRef" class="gauge-chart"></div>
-            <div class="chart-label">
-              <span class="label-dot label-dot-orange"></span>
-              <span class="label-text">处理完成</span>
-            </div>
-          </el-col>
-          <el-col :span="6" class="chart-item">
-            <div ref="transOrderChartRef" class="gauge-chart"></div>
-            <div class="chart-label">
-              <span class="label-dot label-dot-purple"></span>
-              <span class="label-text">转工单</span>
-            </div>
-          </el-col>
-          <el-col :span="6" class="chart-item">
-            <div ref="orderFinishChartRef" class="gauge-chart"></div>
-            <div class="chart-label">
-              <span class="label-dot label-dot-cyan"></span>
-              <span class="label-text">工单处理完成</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-    <el-col :span="12">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot"></div>
-              <span class="chart-title">维修工单状态统计</span>
-            </div>
-          </div>
-        </template>
-        <el-row class="chart-grid">
-          <el-col :span="12" class="chart-item">
-            <div ref="writeChartRef" class="gauge-chart"></div>
-            <div class="chart-label">
-              <span class="label-dot label-dot-indigo"></span>
-              <span class="label-text">待填写</span>
-            </div>
-          </el-col>
-          <el-col :span="12" class="chart-item">
-            <div ref="finishedChartRef" class="gauge-chart"></div>
-            <div class="chart-label">
-              <span class="label-dot label-dot-emerald"></span>
-              <span class="label-text">已完成</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第三行:消息统计行 -->
-  <el-row>
-    <el-col :span="24">
-      <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
-        <template #header>
-          <div class="chart-header">
-            <div class="chart-title-wrapper">
-              <div class="chart-title-dot"></div>
-              <span class="chart-title">近一年数量统计</span>
-            </div>
-          </div>
-        </template>
-        <div ref="chartContainer" class="bar-chart-container"></div>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- TODO 第四行:地图 -->
+  <div ref="wrapperRef" class="maintain-kb-wrapper bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div class="maintain-kb-screen bg" :style="targetAreaStyle">
+        <header class="header">维修统计</header>
+
+        <main class="maintain-kb-content">
+          <section class="summary-grid">
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:data-line" class="stat-icon" />
+              </span>
+              <span class="stat-title">MTTR</span>
+              <strong class="stat-value">{{ statsData.productCategoryCount }}</strong>
+              <span class="stat-label">平均解决时间</span>
+            </button>
+
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:trend-charts" class="stat-icon" />
+              </span>
+              <span class="stat-title">近一周工单数量</span>
+              <span class="stat-pair">
+                <span>
+                  <em>故障上报</em>
+                  <strong>{{ week.failureWeek }}</strong>
+                </span>
+                <span>
+                  <em>维修工单</em>
+                  <strong>{{ week.maintainWeek }}</strong>
+                </span>
+              </span>
+            </button>
+
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:calendar" class="stat-icon" />
+              </span>
+              <span class="stat-title">近一月工单数量</span>
+              <span class="stat-pair">
+                <span>
+                  <em>故障上报</em>
+                  <strong>{{ month.failureMonth }}</strong>
+                </span>
+                <span>
+                  <em>维修工单</em>
+                  <strong>{{ month.maintainMonth }}</strong>
+                </span>
+              </span>
+            </button>
+
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:pie-chart" class="stat-icon" />
+              </span>
+              <span class="stat-title">工单总数量</span>
+              <span class="stat-pair">
+                <span>
+                  <em>故障上报</em>
+                  <strong>{{ total.failureTotal }}</strong>
+                </span>
+                <span>
+                  <em>维修工单</em>
+                  <strong>{{ total.maintainTotal }}</strong>
+                </span>
+              </span>
+            </button>
+          </section>
+
+          <section class="status-grid">
+            <el-card class="panel chart-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator">
+                  <span></span>
+                  <span></span>
+                </div>
+                故障上报状态统计
+              </div>
+              <div class="gauge-grid gauge-grid--failure">
+                <div class="chart-item">
+                  <div ref="reportingChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-blue"></span>
+                    <span class="label-text">上报中</span>
+                  </div>
+                </div>
+                <div class="chart-item">
+                  <div ref="dealFinishedChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-orange"></span>
+                    <span class="label-text">处理完成</span>
+                  </div>
+                </div>
+                <div class="chart-item">
+                  <div ref="transOrderChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-purple"></span>
+                    <span class="label-text">转工单</span>
+                  </div>
+                </div>
+                <div class="chart-item">
+                  <div ref="orderFinishChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-cyan"></span>
+                    <span class="label-text">工单处理完成</span>
+                  </div>
+                </div>
+              </div>
+            </el-card>
+
+            <el-card class="panel chart-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator">
+                  <span></span>
+                  <span></span>
+                </div>
+                维修工单状态统计
+              </div>
+              <div class="gauge-grid gauge-grid--maintain">
+                <div class="chart-item">
+                  <div ref="writeChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-indigo"></span>
+                    <span class="label-text">待填写</span>
+                  </div>
+                </div>
+                <div class="chart-item">
+                  <div ref="finishedChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-emerald"></span>
+                    <span class="label-text">已完成</span>
+                  </div>
+                </div>
+              </div>
+            </el-card>
+          </section>
+
+          <section class="trend-section">
+            <el-card class="panel trend-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator">
+                  <span></span>
+                  <span></span>
+                </div>
+                近一年数量统计
+              </div>
+              <div ref="chartContainer" class="bar-chart-container"></div>
+            </el-card>
+          </section>
+        </main>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts" name="Index">
@@ -203,6 +173,7 @@ import {
 } from '@/api/iot/statistics'
 import { formatDate } from '@/utils/formatTime'
 import { IotStatApi } from '@/api/pms/stat'
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
 
 // TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
 
@@ -232,6 +203,37 @@ const queryParams = reactive({
   startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
   endTime: Date.now() // 设置默认结束时间为当前时间
 })
+const wrapperRef = ref<HTMLDivElement>()
+const scale = ref(1)
+
+let resizeObserver: ResizeObserver | null = null
+let resizeRaf = 0
+
+const targetWrapperStyle = computed(() => ({
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const targetAreaStyle = computed(() => ({
+  '--kb-scale': scale.value,
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+function updateScale() {
+  cancelAnimationFrame(resizeRaf)
+
+  resizeRaf = requestAnimationFrame(() => {
+    const wrapper = wrapperRef.value
+    if (!wrapper) return
+
+    const { clientWidth, clientHeight } = wrapper
+    if (!clientWidth || !clientHeight) return
+
+    scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(handleResize)
+  })
+}
 
 // const deviceCountChartRef = ref() // 设备数量统计的图表
 const reportingChartRef = ref() // 在线设备统计的图表
@@ -241,6 +243,7 @@ const orderFinishChartRef = ref()
 const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
 const writeChartRef = ref() // 上下行消息量统计的图表
 const finishedChartRef = ref() // 上下行消息量统计的图表
+const chartInstances: echarts.ECharts[] = []
 
 // 基础统计数据
 // TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
@@ -357,7 +360,6 @@ const getStats = async () => {
   })
   IotStatApi.getMainStatus().then((res) => {
     status.value = res
-    console.log(JSON.stringify(status.value))
     initCharts()
   })
   // statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
@@ -371,6 +373,7 @@ const getStats = async () => {
 
 /** 初始化图表 */
 const initCharts = () => {
+  disposeChartInstances()
   // 设备数量统计
   // echarts.init(deviceCountChartRef.value).setOption({
   //   tooltip: {
@@ -456,7 +459,11 @@ const initCharts = () => {
 
 /** 初始化仪表盘图表 */
 const initGaugeChart = (el: any, value: number, color: string) => {
-  echarts.init(el).setOption({
+  if (!el) return
+
+  const chart = echarts.init(el)
+  chartInstances.push(chart)
+  chart.setOption({
     series: [
       {
         type: 'gauge',
@@ -620,7 +627,11 @@ const initMessageChart = () => {
 }
 
 const chartContainer = ref(null)
-let chartInstance = null
+let chartInstance: echarts.ECharts | null = null
+
+const disposeChartInstances = () => {
+  chartInstances.splice(0).forEach((instance) => instance.dispose())
+}
 
 // 生成过去12个月份的标签 (格式: YYYY-MM)
 const generateMonthLabels = () => {
@@ -731,49 +742,87 @@ const initChart = async () => {
   }
 
   // 初始化图表
+  chartInstance?.dispose()
   chartInstance = echarts.init(chartContainer.value)
   chartInstance.setOption(option)
 
-  // 窗口缩放监听
-  window.addEventListener('resize', handleResize)
   handleResize()
 }
 
 // 自适应调整
 const handleResize = () => {
   chartInstance?.resize()
+  chartInstances.forEach((instance) => instance.resize())
 }
 /** 初始化 */
 onMounted(() => {
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  window.addEventListener('resize', updateScale)
+
   getStats()
   initChart()
 })
 onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  cancelAnimationFrame(resizeRaf)
+  disposeChartInstances()
   chartInstance?.dispose()
-  window.removeEventListener('resize', handleResize)
+  chartInstance = null
 })
 </script>
 
 <style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+// 响应式优化
+@media (width <= 1200px) {
+  .stat-value {
+    font-size: 28px;
+  }
+
+  .stat-sub-value {
+    font-size: 24px;
+  }
+}
+
+@media (width <= 768px) {
+  .stat-card {
+    margin-bottom: 16px;
+  }
+
+  .chart-item {
+    padding: 8px;
+  }
+
+  .gauge-chart {
+    height: 140px;
+  }
+}
+
 // 统计卡片基础样式
 .stat-card {
-  border-radius: 12px;
-  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%);
+  overflow: hidden;
+  background: linear-gradient(180deg, #fff 0%, #f4f8ff 100%);
+  border: 1px solid rgb(207 220 237 / 90%);
+  border-radius: 12px;
   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);
+    inset 0 1px 0 rgb(255 255 255 / 95%),
+    inset 0 -10px 24px rgb(210 225 244 / 26%),
+    0 10px 24px rgb(32 66 120 / 8%);
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
   &:hover {
     transform: translateY(-4px);
     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);
+      inset 0 1px 0 rgb(255 255 255 / 98%),
+      inset 0 -12px 26px rgb(204 220 243 / 32%),
+      0 16px 32px rgb(32 66 120 / 12%);
   }
 
   :deep(.el-card__body) {
@@ -783,23 +832,23 @@ onUnmounted(() => {
 
 // 渐变色背景
 .stat-card-gradient-1 {
-  background: radial-gradient(circle at top left, rgba(121, 164, 255, 0.16), transparent 42%),
-    linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
+  background: radial-gradient(circle at top left, rgb(121 164 255 / 16%), transparent 42%),
+    linear-gradient(180deg, #fff 0%, #f4f8ff 100%);
 }
 
 .stat-card-gradient-2 {
-  background: radial-gradient(circle at top left, rgba(118, 186, 255, 0.14), transparent 42%),
-    linear-gradient(180deg, #ffffff 0%, #f3f8ff 100%);
+  background: radial-gradient(circle at top left, rgb(118 186 255 / 14%), transparent 42%),
+    linear-gradient(180deg, #fff 0%, #f3f8ff 100%);
 }
 
 .stat-card-gradient-3 {
-  background: radial-gradient(circle at top left, rgba(96, 154, 241, 0.14), transparent 42%),
-    linear-gradient(180deg, #ffffff 0%, #f5f9ff 100%);
+  background: radial-gradient(circle at top left, rgb(96 154 241 / 14%), transparent 42%),
+    linear-gradient(180deg, #fff 0%, #f5f9ff 100%);
 }
 
 .stat-card-gradient-4 {
-  background: radial-gradient(circle at top left, rgba(137, 176, 242, 0.14), transparent 42%),
-    linear-gradient(180deg, #ffffff 0%, #f4f8fe 100%);
+  background: radial-gradient(circle at top left, rgb(137 176 242 / 14%), transparent 42%),
+    linear-gradient(180deg, #fff 0%, #f4f8fe 100%);
 }
 
 // 统计内容区域
@@ -816,18 +865,18 @@ onUnmounted(() => {
 }
 
 .stat-icon-wrapper {
+  display: flex;
   width: 48px;
   height: 48px;
-  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);
+  background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(232 240 252 / 92%) 100%);
+  border: 1px solid rgb(205 219 239 / 90%);
   border-radius: 12px;
-  display: flex;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 95%),
+    inset 0 -6px 12px rgb(205 220 242 / 34%),
+    0 6px 14px rgb(66 104 168 / 8%);
   align-items: center;
   justify-content: center;
-  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 {
@@ -842,8 +891,8 @@ onUnmounted(() => {
 }
 
 .stat-divider {
-  border-color: rgba(201, 214, 234, 0.85);
   margin: 12px 0;
+  border-color: rgb(201 214 234 / 85%);
 }
 
 .stat-body {
@@ -853,16 +902,16 @@ onUnmounted(() => {
 }
 
 .stat-label {
+  margin-bottom: 4px;
   font-size: 13px;
   color: #7182a1;
-  margin-bottom: 4px;
 }
 
 .stat-value {
   font-size: 36px;
   font-weight: 700;
-  color: #1f2f54;
   line-height: 1;
+  color: #1f2f54;
 }
 
 .stat-item {
@@ -885,32 +934,28 @@ onUnmounted(() => {
 
 // 图表卡片增强样式
 .chart-card-enhanced {
+  background: linear-gradient(180deg, #fff 0%, #f5f9ff 100%);
+  border: 1px solid rgb(207 220 237 / 90%);
   border-radius: 12px;
-  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);
+    inset 0 1px 0 rgb(255 255 255 / 96%),
+    inset 0 -12px 26px rgb(212 226 244 / 20%),
+    0 10px 24px rgb(32 66 120 / 7%);
+  transition: all 0.3s ease;
 
   &:hover {
+    border-color: rgb(193 209 231 / 95%);
     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);
+      inset 0 1px 0 rgb(255 255 255 / 98%),
+      inset 0 -14px 30px rgb(208 223 244 / 24%),
+      0 16px 30px rgb(32 66 120 / 10%);
   }
 
   :deep(.el-card__header) {
     padding: 16px 20px;
-    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);
+    background: linear-gradient(90deg, rgb(244 248 255 / 96%) 0%, rgb(255 255 255 / 98%) 100%);
+    border-bottom: 1px solid rgb(223 232 245 / 95%);
+    box-shadow: inset 0 -1px 0 rgb(255 255 255 / 78%);
   }
 
   :deep(.el-card__body) {
@@ -933,18 +978,18 @@ onUnmounted(() => {
 .chart-title-dot {
   width: 8px;
   height: 8px;
-  border-radius: 50%;
   background: linear-gradient(135deg, #84aef7 0%, #5e87d8 100%);
+  border-radius: 50%;
   box-shadow:
-    0 0 0 4px rgba(122, 162, 235, 0.12),
-    0 2px 6px rgba(86, 124, 189, 0.24);
+    0 0 0 4px rgb(122 162 235 / 12%),
+    0 2px 6px rgb(86 124 189 / 24%);
 }
 
 .chart-title {
   font-size: 16px;
   font-weight: 600;
-  color: #233554;
   letter-spacing: 0.3px;
+  color: #233554;
 }
 
 .chart-grid {
@@ -953,21 +998,17 @@ onUnmounted(() => {
 
 .chart-item {
   display: flex;
-  flex-direction: column;
-  align-items: center;
   padding: 12px;
-  transition: all 0.2s ease;
   border-radius: 12px;
+  transition: all 0.2s ease;
+  flex-direction: column;
+  align-items: center;
 
   &:hover {
-    background: linear-gradient(
-      180deg,
-      rgba(244, 248, 255, 0.9) 0%,
-      rgba(237, 244, 255, 0.95) 100%
-    );
+    background: linear-gradient(180deg, rgb(244 248 255 / 90%) 0%, rgb(237 244 255 / 95%) 100%);
     box-shadow:
-      inset 0 1px 0 rgba(255, 255, 255, 0.86),
-      inset 0 -8px 16px rgba(212, 225, 243, 0.22);
+      inset 0 1px 0 rgb(255 255 255 / 86%),
+      inset 0 -8px 16px rgb(212 225 243 / 22%);
   }
 }
 
@@ -978,103 +1019,315 @@ onUnmounted(() => {
 
 .chart-label {
   display: flex;
-  align-items: center;
-  gap: 6px;
-  margin-top: 8px;
   padding: 6px 12px;
-  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);
+  margin-top: 8px;
+  background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(242 247 255 / 96%) 100%);
+  border: 1px solid rgb(216 228 244 / 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);
+    inset 0 1px 0 rgb(255 255 255 / 90%),
+    0 4px 10px rgb(58 90 145 / 6%);
+  transition: all 0.2s ease;
+  align-items: center;
+  gap: 6px;
 
   .chart-item:hover & {
-    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);
+    background: linear-gradient(180deg, rgb(255 255 255 / 98%) 0%, rgb(238 244 255 / 98%) 100%);
+    border-color: rgb(201 217 239 / 98%);
   }
 }
 
 .label-dot {
+  display: inline-block;
   width: 8px;
   height: 8px;
   border-radius: 50%;
-  display: inline-block;
 }
 
 .label-dot-blue {
   background: #3b82f6;
-  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+  box-shadow: 0 0 0 2px rgb(59 130 246 / 20%);
 }
 
 .label-dot-orange {
   background: #f97316;
-  box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2);
+  box-shadow: 0 0 0 2px rgb(249 115 22 / 20%);
 }
 
 .label-dot-purple {
   background: #8b5cf6;
-  box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
+  box-shadow: 0 0 0 2px rgb(139 92 246 / 20%);
 }
 
 .label-dot-cyan {
   background: #06b6d4;
-  box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
+  box-shadow: 0 0 0 2px rgb(6 182 212 / 20%);
 }
 
 .label-dot-indigo {
   background: #6366f1;
-  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
+  box-shadow: 0 0 0 2px rgb(99 102 241 / 20%);
 }
 
 .label-dot-emerald {
   background: #10b981;
-  box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
+  box-shadow: 0 0 0 2px rgb(16 185 129 / 20%);
 }
 
 .label-text {
   font-size: 13px;
-  color: #64748f;
   font-weight: 500;
+  color: #64748f;
 }
 
 .bar-chart-container {
-  height: 350px;
   width: 100%;
+  height: 350px;
+  background: linear-gradient(180deg, rgb(252 254 255 / 90%) 0%, rgb(244 249 255 / 92%) 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);
+    inset 0 1px 0 rgb(255 255 255 / 92%),
+    inset 0 -10px 20px rgb(217 230 245 / 16%);
 }
 
-// 响应式优化
-@media (max-width: 1200px) {
-  .stat-value {
-    font-size: 28px;
+.maintain-kb-wrapper {
+  overflow: hidden;
+}
+
+.maintain-kb-screen {
+  display: flex;
+  overflow: hidden;
+  color: #24364f;
+  flex-direction: column;
+}
+
+.maintain-kb-content {
+  display: flex;
+  height: calc(100% - 52px * var(--kb-scale));
+  min-height: 0;
+  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
+  flex-direction: column;
+  gap: calc(12px * var(--kb-scale));
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: calc(12px * var(--kb-scale));
+  flex: 0 0 calc(154px * var(--kb-scale));
+}
+
+.status-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: calc(12px * var(--kb-scale));
+  flex: 0 0 calc(300px * var(--kb-scale));
+  min-height: 0;
+}
+
+.trend-section {
+  min-height: 0;
+  flex: 1;
+}
+
+.chart-panel,
+.trend-panel {
+  height: 100%;
+  overflow: hidden;
+
+  :deep(.el-card__body) {
+    display: flex;
+    height: 100%;
+    min-height: 0;
+    padding: 0;
+    flex-direction: column;
   }
+}
 
-  .stat-sub-value {
-    font-size: 24px;
+.stat-card {
+  display: grid;
+  height: 100%;
+  min-width: 0;
+  padding: calc(14px * var(--kb-scale));
+  text-align: left;
+  cursor: default;
+  background: linear-gradient(180deg, rgb(255 255 255 / 42%) 0%, rgb(213 227 249 / 28%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(16px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale)) calc(14px * var(--kb-scale)) rgb(63 103 171 / 7%);
+  grid-template-rows: auto auto 1fr;
+  grid-template-columns: auto 1fr;
+  gap: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale));
+
+  &:hover {
+    border-color: rgb(76 132 214 / 26%);
+    transform: translateY(calc(-2px * var(--kb-scale)));
+    box-shadow:
+      inset 0 1px 0 rgb(255 255 255 / 84%),
+      0 calc(10px * var(--kb-scale)) calc(20px * var(--kb-scale)) rgb(63 103 171 / 11%);
   }
 }
 
-@media (max-width: 768px) {
-  .stat-card {
-    margin-bottom: 16px;
+.stat-icon-wrapper {
+  display: flex;
+  width: calc(44px * var(--kb-scale));
+  height: calc(44px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(232 240 252 / 92%) 100%);
+  border: 1px solid rgb(205 219 239 / 90%);
+  border-radius: calc(12px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 95%),
+    inset 0 calc(-6px * var(--kb-scale)) calc(12px * var(--kb-scale)) rgb(205 220 242 / 34%),
+    0 calc(6px * var(--kb-scale)) calc(14px * var(--kb-scale)) rgb(66 104 168 / 8%);
+  align-items: center;
+  justify-content: center;
+  grid-row: 1 / span 2;
+}
+
+.stat-icon {
+  font-size: calc(24px * var(--kb-scale));
+  color: #1f5bb8;
+}
+
+.stat-title {
+  min-width: 0;
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(22px * var(--kb-scale));
+  font-weight: normal;
+  line-height: 1.1;
+  color: #03409b;
+}
+
+.stat-label {
+  font-size: calc(13px * var(--kb-scale));
+  font-weight: 700;
+  color: #536986;
+}
+
+.stat-value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(42px * var(--kb-scale));
+  font-weight: normal;
+  line-height: 1;
+  color: #1f5bb8;
+  grid-column: 1 / -1;
+}
+
+.stat-pair {
+  display: grid;
+  min-width: 0;
+  grid-column: 1 / -1;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: calc(10px * var(--kb-scale));
+
+  span {
+    display: flex;
+    min-width: 0;
+    padding: calc(8px * var(--kb-scale)) calc(10px * var(--kb-scale));
+    overflow: hidden;
+    background: linear-gradient(180deg, rgb(255 255 255 / 30%) 0%, rgb(213 227 249 / 18%) 100%);
+    border: 1px solid rgb(255 255 255 / 48%);
+    border-radius: calc(10px * var(--kb-scale));
+    flex-direction: column;
+    gap: calc(6px * var(--kb-scale));
   }
 
-  .chart-item {
-    padding: 8px;
+  em {
+    overflow: hidden;
+    font-size: calc(12px * var(--kb-scale));
+    font-style: normal;
+    font-weight: 700;
+    color: #536986;
+    text-overflow: ellipsis;
+    white-space: nowrap;
   }
 
-  .gauge-chart {
-    height: 140px;
+  strong {
+    font-family: YouSheBiaoTiHei, sans-serif;
+    font-size: calc(28px * var(--kb-scale));
+    font-weight: normal;
+    line-height: 1;
+    color: #1f5bb8;
   }
 }
+
+.gauge-grid {
+  display: grid;
+  min-height: 0;
+  padding: calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale)) calc(16px * var(--kb-scale));
+  gap: calc(12px * var(--kb-scale));
+  flex: 1;
+}
+
+.gauge-grid--failure {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.gauge-grid--maintain {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.chart-item {
+  display: flex;
+  min-width: 0;
+  min-height: 0;
+  padding: calc(8px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 42%) 0%, rgb(213 227 249 / 28%) 100%);
+  border: 1px solid rgb(255 255 255 / 54%);
+  border-radius: calc(14px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 64%),
+    0 calc(5px * var(--kb-scale)) calc(12px * var(--kb-scale)) rgb(63 103 171 / 6%);
+  align-items: center;
+  flex-direction: column;
+}
+
+.gauge-chart {
+  width: 100%;
+  min-height: 0;
+  flex: 1;
+}
+
+.chart-label {
+  display: flex;
+  height: calc(28px * var(--kb-scale));
+  padding: 0 calc(10px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 88%) 0%, rgb(242 247 255 / 76%) 100%);
+  border: 1px solid rgb(216 228 244 / 78%);
+  border-radius: calc(999px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 90%),
+    0 calc(4px * var(--kb-scale)) calc(10px * var(--kb-scale)) rgb(58 90 145 / 6%);
+  align-items: center;
+  gap: calc(6px * var(--kb-scale));
+}
+
+.label-dot {
+  width: calc(8px * var(--kb-scale));
+  height: calc(8px * var(--kb-scale));
+}
+
+.label-text {
+  overflow: hidden;
+  font-size: calc(13px * var(--kb-scale));
+  font-weight: 700;
+  color: #536986;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.bar-chart-container {
+  width: 100%;
+  min-height: 0;
+  padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale));
+  background: transparent;
+  box-shadow: none;
+  flex: 1;
+}
+
+:deep(.el-card) {
+  --el-card-bg-color: transparent;
+}
 </style>

+ 71 - 11
src/views/pms/stat/rdkb/rd-rate.vue

@@ -8,19 +8,29 @@ import {
   createTooltip,
   FONT_FAMILY,
   formatDateLabel,
+  formatMonthLabel,
   THEME
 } from '@/utils/kb'
 import { IotStatApi } from '@/api/pms/stat'
 
+type RateRange = 'week' | 'sixMonth'
+
 const chartData = ref<ChartData>({
   xAxis: [],
   series: []
 })
 
 const chartRef = ref<HTMLDivElement>()
+const activeRange = ref<RateRange>('week')
+const loading = ref(false)
+const rangeOptions: Array<{ label: string; value: RateRange }> = [
+  { label: '近一周', value: 'week' },
+  { label: '近六个月', value: 'sixMonth' }
+]
 let chart: echarts.ECharts | null = null
 const router = useRouter()
 let chartClickBound = false
+let loadRequestId = 0
 
 function formatRate(value: number) {
   return `${Number(value || 0).toFixed(2)}%`
@@ -57,7 +67,7 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
         fontWeight: 500,
         fontFamily: FONT_FAMILY,
         formatter(value: string) {
-          return formatDateLabel(value)
+          return activeRange.value === 'sixMonth' ? formatMonthLabel(value) : formatDateLabel(value)
         }
       }
     },
@@ -152,9 +162,10 @@ function getChartDayRange() {
   const endDate = dayjs(xAxis[xAxis.length - 1])
   if (!startDate.isValid() || !endDate.isValid()) return null
 
+  const rangeUnit = activeRange.value === 'sixMonth' ? 'month' : 'day'
   return [
-    startDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
-    endDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+    startDate.startOf(rangeUnit).format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf(rangeUnit).format('YYYY-MM-DD HH:mm:ss')
   ]
 }
 
@@ -191,9 +202,16 @@ function destroyChart() {
   }
 }
 
-async function loadChart() {
+async function loadChart(range: RateRange = activeRange.value) {
+  const requestId = ++loadRequestId
+  loading.value = true
   try {
-    const res = await IotStatApi.getRdSevenDayUtilization()
+    const res =
+      range === 'sixMonth'
+        ? await IotStatApi.getRdSixMonthUtilization()
+        : await IotStatApi.getRdSevenDayUtilization()
+    if (requestId !== loadRequestId) return
+
     chartData.value = {
       xAxis: res.xAxis || [],
       series: (res.series || []).map((item) => ({
@@ -203,15 +221,24 @@ async function loadChart() {
     }
     renderChart()
   } catch (error) {
+    if (requestId !== loadRequestId) return
     console.error('获取设备利用率失败:', error)
     chartData.value = {
       xAxis: [],
       series: []
     }
     renderChart()
+  } finally {
+    if (requestId === loadRequestId) {
+      loading.value = false
+    }
   }
 }
 
+watch(activeRange, (range) => {
+  loadChart(range)
+})
+
 onMounted(() => {
   initChart()
   loadChart()
@@ -228,17 +255,50 @@ onUnmounted(() => {
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title">
-      <div class="icon-decorator">
-        <span></span>
-        <span></span>
+    <div class="panel-title flex items-center justify-between">
+      <div class="kb-panel-title-text flex items-center">
+        <div class="icon-decorator">
+          <span></span>
+          <span></span>
+        </div>
+        设备利用率
       </div>
-      设备利用率
+      <el-segmented
+        v-model="activeRange"
+        :options="rangeOptions"
+        size="small"
+        class="rate-range-switch" />
     </div>
-    <div ref="chartRef" class="flex-1 min-h-0"></div>
+    <div ref="chartRef" v-loading="loading" class="rate-chart flex-1 min-h-0"></div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.rate-range-switch {
+  --el-segmented-item-selected-color: #03409b;
+  --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
+  --el-segmented-bg-color: rgb(31 91 184 / 10%);
+  --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
+
+  min-height: calc(26px * var(--kb-scale, 1));
+  padding: calc(2px * var(--kb-scale, 1));
+  border: 1px solid rgb(31 91 184 / 12%);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
+
+  :deep(.el-segmented__item) {
+    min-height: calc(22px * var(--kb-scale, 1));
+    padding: 0 calc(7px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.rate-chart {
+  :deep(.el-loading-mask) {
+    background-color: rgb(221 233 251 / 42%);
+  }
+}
 </style>

+ 71 - 11
src/views/pms/stat/rhkb/equipment-rate.vue

@@ -8,19 +8,29 @@ import {
   createTooltip,
   FONT_FAMILY,
   formatDateLabel,
+  formatMonthLabel,
   THEME
 } from '@/utils/kb'
 import { IotStatApi } from '@/api/pms/stat'
 
+type RateRange = 'week' | 'sixMonth'
+
 const chartData = ref<ChartData>({
   xAxis: [],
   series: []
 })
 
 const chartRef = ref<HTMLDivElement>()
+const activeRange = ref<RateRange>('week')
+const loading = ref(false)
+const rangeOptions: Array<{ label: string; value: RateRange }> = [
+  { label: '近一周', value: 'week' },
+  { label: '近六个月', value: 'sixMonth' }
+]
 let chart: echarts.ECharts | null = null
 const router = useRouter()
 let chartClickBound = false
+let loadRequestId = 0
 
 function formatRate(value: number) {
   return `${Number(value || 0).toFixed(2)}%`
@@ -57,7 +67,7 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
         fontWeight: 500,
         fontFamily: FONT_FAMILY,
         formatter(value: string) {
-          return formatDateLabel(value)
+          return activeRange.value === 'sixMonth' ? formatMonthLabel(value) : formatDateLabel(value)
         }
       }
     },
@@ -152,9 +162,10 @@ function getChartDayRange() {
   const endDate = dayjs(xAxis[xAxis.length - 1])
   if (!startDate.isValid() || !endDate.isValid()) return null
 
+  const rangeUnit = activeRange.value === 'sixMonth' ? 'month' : 'day'
   return [
-    startDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
-    endDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+    startDate.startOf(rangeUnit).format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf(rangeUnit).format('YYYY-MM-DD HH:mm:ss')
   ]
 }
 
@@ -191,9 +202,16 @@ function destroyChart() {
   }
 }
 
-async function loadChart() {
+async function loadChart(range: RateRange = activeRange.value) {
+  const requestId = ++loadRequestId
+  loading.value = true
   try {
-    const res = await IotStatApi.getRhSevenDayUtilization()
+    const res =
+      range === 'sixMonth'
+        ? await IotStatApi.getRhSixMonthUtilization()
+        : await IotStatApi.getRhSevenDayUtilization()
+    if (requestId !== loadRequestId) return
+
     chartData.value = {
       xAxis: res.xAxis || [],
       series: (res.series || []).map((item) => ({
@@ -203,15 +221,24 @@ async function loadChart() {
     }
     renderChart()
   } catch (error) {
+    if (requestId !== loadRequestId) return
     console.error('获取设备利用率失败:', error)
     chartData.value = {
       xAxis: [],
       series: []
     }
     renderChart()
+  } finally {
+    if (requestId === loadRequestId) {
+      loading.value = false
+    }
   }
 }
 
+watch(activeRange, (range) => {
+  loadChart(range)
+})
+
 onMounted(() => {
   initChart()
   loadChart()
@@ -228,17 +255,50 @@ onUnmounted(() => {
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title">
-      <div class="icon-decorator">
-        <span></span>
-        <span></span>
+    <div class="panel-title flex items-center justify-between">
+      <div class="kb-panel-title-text flex items-center">
+        <div class="icon-decorator">
+          <span></span>
+          <span></span>
+        </div>
+        设备利用率
       </div>
-      设备利用率
+      <el-segmented
+        v-model="activeRange"
+        :options="rangeOptions"
+        size="small"
+        class="rate-range-switch" />
     </div>
-    <div ref="chartRef" class="flex-1 min-h-0"></div>
+    <div ref="chartRef" v-loading="loading" class="rate-chart flex-1 min-h-0"></div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.rate-range-switch {
+  --el-segmented-item-selected-color: #03409b;
+  --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
+  --el-segmented-bg-color: rgb(31 91 184 / 10%);
+  --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
+
+  min-height: calc(26px * var(--kb-scale, 1));
+  padding: calc(2px * var(--kb-scale, 1));
+  border: 1px solid rgb(31 91 184 / 12%);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
+
+  :deep(.el-segmented__item) {
+    min-height: calc(22px * var(--kb-scale, 1));
+    padding: 0 calc(7px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.rate-chart {
+  :deep(.el-loading-mask) {
+    background-color: rgb(221 233 251 / 42%);
+  }
+}
 </style>

+ 4 - 6
src/views/pms/stat/rhkb/rhsummary.vue

@@ -19,21 +19,21 @@ type CardKey =
 type CardConfig = SummaryCardConfig<CardKey>
 
 const cardConfigs: CardConfig[] = [
-    {
+  {
     key: 'todayGas',
     title: '当日注气量(万方)',
     icon: 'i-solar:gas-station-linear',
     accent: THEME.color.blue.strong,
     glow: THEME.color.blue.glow
   },
-   {
+  {
     key: 'yearGas',
     title: '累计注气量(万方)',
     icon: 'i-solar:database-linear',
     accent: THEME.color.green.strong,
     glow: THEME.color.green.glow
   },
-    {
+  {
     key: 'utilizationRate',
     title: '设备利用率(%)',
     icon: 'i-solar:chart-2-linear',
@@ -48,8 +48,6 @@ const cardConfigs: CardConfig[] = [
     glow: THEME.color.blue.glow
   },
 
-
- 
   // {
   //   key: 'maintain',
   //   title: '维修工单',
@@ -354,7 +352,7 @@ onMounted(() => {
         <span></span>
         <span></span>
       </div>
-      工单情
+      运行概
     </div>
 
     <div class="summary-panel__grid grid grid-cols-7 flex-1">

Неке датотеке нису приказане због велике количине промена