1
0

20 Ревизии b895b812d4 ... bbdac5477f

Автор SHA1 Съобщение Дата
  Zimo bbdac5477f Merge branch 'refactor/page' преди 19 часа
  Zimo da340f82c0 表格封装调整 操作 查询 排序 只影响表头排布而不影响表格内容 преди 21 часа
  Zimo 1d812c31bd 页面调整添加表格 action преди 21 часа
  Zimo b0219776bd 调整页面任务查看瑞都日报,添加生成日报按钮 преди 22 часа
  Zimo 6d2bf8e491 工单完成情况 运维成本 преди 2 дни
  Zimo 608882a839 项目信息 преди 2 дни
  Zimo 6441332218 任务查看 query 区调整排版 преди 3 дни
  Zimo 9c2b1c961a 任务查看 преди 3 дни
  Zimo 5458d5b537 物料领用 преди 3 дни
  Zimo 373c77883f 本地库存 преди 3 дни
  Zimo 469e6cd378 配置安全库存 преди 3 дни
  Zimo 96fec57146 SPA库存管理 преди 3 дни
  Zimo ea0b864cf0 调整树形选择又调用调整row span преди 3 дни
  Zimo 4d09933f32 保养查询 преди 3 дни
  Zimo 8da6a27408 保养工单 преди 3 дни
  Zimo 509e9fd4f8 保养计划 преди 3 дни
  Zimo 4b9bc57e49 设备调拨 преди 3 дни
  Zimo 981bcdaf08 设备状态调整 преди 3 дни
  Zimo 3dec6d7fa1 设备责任人页面调整,两个树选择器调整 row-span 由外部传入 преди 3 дни
  Zimo 2f0aa3d12d 设备属性模版页面优化 преди 3 дни
променени са 47 файла, в които са добавени 5771 реда и са изтрити 5066 реда
  1. 53 14
      src/components/DeptTreeSelect/index.vue
  2. 221 0
      src/components/DeviceCategoryTree/index.vue
  3. 1 1
      src/components/WellSelect/index.vue
  4. 8 7
      src/components/ZmTable/ZmTableColumn.vue
  5. 1 1
      src/locales/zh-CN.ts
  6. 1 0
      src/views/oli-connection/monitoring-query/index.vue
  7. 1 0
      src/views/oli-connection/monitoring/index.vue
  8. 432 314
      src/views/pms/device/allotlog/DeviceAllot.vue
  9. 253 264
      src/views/pms/device/personlog/DevicePerson.vue
  10. 428 324
      src/views/pms/device/statuslog/DeviceStatus.vue
  11. 279 227
      src/views/pms/devicetemplate/index.vue
  12. 459 535
      src/views/pms/iotlockstock/index.vue
  13. 401 239
      src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue
  14. 520 680
      src/views/pms/iotmainworkorder/index.vue
  15. 319 150
      src/views/pms/iotmaterialrequisition/index.vue
  16. 348 536
      src/views/pms/iotprojectinfo/index.vue
  17. 548 723
      src/views/pms/iotprojecttask/index.vue
  18. 1 0
      src/views/pms/iotrddailyreport/components/DailyStatistics.vue
  19. 1 0
      src/views/pms/iotrddailyreport/components/NonProductionEfficiency.vue
  20. 8 2
      src/views/pms/iotrddailyreport/fillDailyReport.vue
  21. 8 2
      src/views/pms/iotrddailyreport/index.vue
  22. 8 2
      src/views/pms/iotrddailyreport/statistics.vue
  23. 1 0
      src/views/pms/iotrddailyreport/summary.vue
  24. 7 1
      src/views/pms/iotrhdailyreport/approval.vue
  25. 7 1
      src/views/pms/iotrhdailyreport/fill.vue
  26. 7 1
      src/views/pms/iotrhdailyreport/index.vue
  27. 2 1
      src/views/pms/iotrhdailyreport/rh-table.vue
  28. 4 1
      src/views/pms/iotrhdailyreport/summary.vue
  29. 7 1
      src/views/pms/iotrydailyreport/approval.vue
  30. 7 1
      src/views/pms/iotrydailyreport/fill.vue
  31. 7 1
      src/views/pms/iotrydailyreport/index.vue
  32. 6 2
      src/views/pms/iotrydailyreport/ry-table.vue
  33. 2 2
      src/views/pms/iotrydailyreport/ry-xj-table.vue
  34. 4 16
      src/views/pms/iotrydailyreport/summary.vue
  35. 7 1
      src/views/pms/iotrydailyreport/xapproval.vue
  36. 7 1
      src/views/pms/iotrydailyreport/xfill.vue
  37. 7 1
      src/views/pms/iotrydailyreport/xjindex.vue
  38. 4 16
      src/views/pms/iotrydailyreport/xsummary.vue
  39. 393 193
      src/views/pms/iotsapstock/IotSapStockConfig.vue
  40. 487 504
      src/views/pms/iotsapstock/index.vue
  41. 373 197
      src/views/pms/maintenance/index.vue
  42. 23 35
      src/views/report-statistics/costs.vue
  43. 23 6
      src/views/report-statistics/daily-report.vue
  44. 20 3
      src/views/report-statistics/rd-daily-report.vue
  45. 20 3
      src/views/report-statistics/ry-daily-report.vue
  46. 20 3
      src/views/report-statistics/ry-xj-daily-report.vue
  47. 27 54
      src/views/report-statistics/work-order-completion.vue

+ 53 - 14
src/components/DeptTreeSelect/index.vue

@@ -11,13 +11,19 @@ interface Tree {
   sort?: number
 }
 
+type RequestApi = 'specifiedSimpleDepts' | 'getSimpleDeptList'
+
 const props = defineProps({
   deptId: { type: Number, required: true },
   modelValue: { type: Number, default: undefined },
   topId: { type: Number, required: true },
   title: { type: String, default: '部门' },
   initSelect: { type: Boolean, default: true },
-  showTitle: { type: Boolean, default: true }
+  showTitle: { type: Boolean, default: true },
+  requestApi: {
+    type: String as PropType<RequestApi>,
+    default: 'specifiedSimpleDepts'
+  }
 })
 
 const emits = defineEmits(['update:modelValue', 'node-click'])
@@ -39,20 +45,47 @@ const sortTreeBySort = (treeNodes: Tree[]) => {
   return sortedNodes
 }
 
+const resolveFallbackId = (depts: Tree[], preferredId: number) => {
+  if (depts.some((item) => item.id === preferredId)) return preferredId
+  if (depts.some((item) => item.id === props.topId)) return props.topId
+  return depts[0]?.id
+}
+
+const loadDeptData = async () => {
+  if (props.requestApi === 'getSimpleDeptList') {
+    const depts = (await DeptApi.getSimpleDeptList()) as Tree[]
+    return {
+      depts,
+      currentId: resolveFallbackId(depts, props.deptId)
+    }
+  }
+
+  let id = props.deptId
+  if (id !== props.topId) {
+    const depts = await DeptApi.specifiedSimpleDepts(props.topId)
+    if (depts.length && !depts.find((item) => item.id === props.deptId)) id = props.topId
+  }
+  const depts = (await DeptApi.specifiedSimpleDepts(id)) as Tree[]
+  return {
+    depts,
+    currentId: id
+  }
+}
+
 const loadTree = async () => {
   try {
-    let id = props.deptId
-    if (id !== props.topId) {
-      const depts = await DeptApi.specifiedSimpleDepts(props.topId)
-      if (depts.length && !depts.find((item) => item.id === props.deptId)) id = props.topId
-    }
-    const res = await DeptApi.specifiedSimpleDepts(id)
-    if (props.initSelect && props.modelValue && !res.some((item) => item.id === props.modelValue)) {
-      emits('update:modelValue', id)
+    const { depts, currentId } = await loadDeptData()
+    if (
+      props.initSelect &&
+      props.modelValue &&
+      currentId !== undefined &&
+      !depts.some((item) => item.id === props.modelValue)
+    ) {
+      emits('update:modelValue', currentId)
     }
-    deptList.value = sortTreeBySort(handleTree(res))
+    deptList.value = sortTreeBySort(handleTree(depts))
     nextTick(() => {
-      const targetKey = props.modelValue ?? (props.initSelect ? id : null)
+      const targetKey = props.modelValue ?? (props.initSelect ? currentId : null)
       if (targetKey && treeRef.value) {
         treeRef.value.setCurrentKey(targetKey)
         if (!expandedKeys.value.includes(targetKey)) expandedKeys.value.push(targetKey)
@@ -73,7 +106,7 @@ const handleNodeClick = (data: Tree) => {
 const filterNode = (val: string, data: Tree) => !val || data.name.includes(val)
 
 watch(deptName, (val) => treeRef.value?.filter(val))
-watch(() => props.deptId, loadTree)
+watch(() => [props.deptId, props.topId, props.requestApi], loadTree)
 watch(
   () => props.modelValue,
   (val) => {
@@ -91,14 +124,20 @@ onMounted(loadTree)
 
 <template>
   <div
-    class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-4 transition-all duration-300 ease-in-out overflow-visible"
+    class="dept-aside-container 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">
       <h1 v-if="showTitle" class="text-lg font-medium truncate shrink-0">{{ props.title }}</h1>
 
       <div class="shrink-0">
-        <el-input v-model="deptName" placeholder="请输入部门名称" clearable :prefix-icon="Search" />
+        <el-input
+          v-model="deptName"
+          placeholder="请输入部门名称"
+          clearable
+          size="default"
+          :prefix-icon="Search"
+        />
       </div>
 
       <div class="flex-1 relative overflow-hidden">

+ 221 - 0
src/components/DeviceCategoryTree/index.vue

@@ -0,0 +1,221 @@
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeviceCategoryApi from '@/api/pms/productclassify'
+import { CaretLeft, CaretRight, Search } from '@element-plus/icons-vue'
+import { ElTree } from 'element-plus'
+
+defineOptions({ name: 'DeviceCategoryTree' })
+
+interface DeviceCategoryNode extends Tree {
+  parentId?: number
+  code?: string
+  status?: number
+}
+
+const props = defineProps({
+  modelValue: {
+    type: Number as PropType<number | undefined>,
+    default: undefined
+  },
+  title: {
+    type: String,
+    default: '设备分类'
+  },
+  rootId: {
+    type: Number,
+    default: 0
+  },
+  rootName: {
+    type: String,
+    default: '顶级设备分类'
+  },
+  initSelect: {
+    type: Boolean,
+    default: false
+  },
+  showTitle: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const emits = defineEmits<{
+  (event: 'update:modelValue', value?: number): void
+  (event: 'node-click', value: DeviceCategoryNode): void
+}>()
+
+const isCollapsed = ref(false)
+const deviceCategoryName = ref('')
+const deviceCategoryList = ref<DeviceCategoryNode[]>([])
+const treeRef = ref<InstanceType<typeof ElTree>>()
+const expandedKeys = ref<number[]>([])
+
+const sortTreeBySort = (treeNodes: DeviceCategoryNode[]) => {
+  if (!Array.isArray(treeNodes)) return treeNodes
+
+  return [...treeNodes]
+    .sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999))
+    .map((node) => ({
+      ...node,
+      children: node.children ? sortTreeBySort(node.children as DeviceCategoryNode[]) : undefined
+    }))
+}
+
+const setCurrentNode = (id?: number) => {
+  if (id === undefined || id === null || !treeRef.value) return
+
+  treeRef.value.setCurrentKey(id)
+  if (!expandedKeys.value.includes(id)) {
+    expandedKeys.value.push(id)
+  }
+}
+
+const loadTree = async () => {
+  try {
+    const res = await DeviceCategoryApi.IotProductClassifyApi.getSimpleProductClassifyList()
+    const rootNode: DeviceCategoryNode = {
+      id: props.rootId,
+      name: props.rootName,
+      children: sortTreeBySort(handleTree(res) as DeviceCategoryNode[])
+    }
+
+    deviceCategoryList.value = [rootNode]
+    expandedKeys.value = [props.rootId]
+
+    await nextTick()
+
+    const targetId = props.modelValue ?? (props.initSelect ? props.rootId : undefined)
+    if (targetId !== undefined) {
+      setCurrentNode(targetId)
+      if (props.initSelect && props.modelValue === undefined) {
+        emits('update:modelValue', targetId)
+      }
+    }
+  } catch (error) {
+    console.error(error)
+  }
+}
+
+const handleNodeClick = (data: DeviceCategoryNode) => {
+  emits('update:modelValue', data.id)
+  emits('node-click', data)
+}
+
+const filterNode = (val: string, data: DeviceCategoryNode) => !val || data.name.includes(val)
+
+watch(deviceCategoryName, (val) => treeRef.value?.filter(val))
+watch(
+  () => props.modelValue,
+  (val) => setCurrentNode(val),
+  { immediate: true }
+)
+
+onMounted(loadTree)
+</script>
+
+<template>
+  <div
+    class="device-category-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">
+      <h1 v-if="showTitle" class="text-lg font-medium truncate shrink-0">{{ title }}</h1>
+
+      <div class="shrink-0">
+        <el-input
+          v-model="deviceCategoryName"
+          placeholder="请输入设备分类名称"
+          clearable
+          size="default"
+          :prefix-icon="Search"
+        />
+      </div>
+
+      <div class="flex-1 relative overflow-hidden">
+        <el-auto-resizer class="absolute">
+          <template #default="{ height }">
+            <el-scrollbar :style="{ height: `${height}px` }">
+              <el-tree
+                ref="treeRef"
+                :data="deviceCategoryList"
+                :props="defaultProps"
+                :expand-on-click-node="false"
+                :filter-node-method="filterNode"
+                :default-expanded-keys="expandedKeys"
+                node-key="id"
+                highlight-current
+                @node-click="handleNodeClick"
+              />
+            </el-scrollbar>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+
+    <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
+      <el-icon size="12">
+        <CaretLeft v-if="!isCollapsed" />
+        <CaretRight v-else />
+      </el-icon>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.device-category-aside {
+  width: 14vw;
+  height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  min-width: 200px;
+  box-sizing: border-box;
+  flex-shrink: 0;
+}
+
+.device-category-aside.is-collapsed {
+  width: 0 !important;
+  min-width: 0 !important;
+  padding: 0 !important;
+  margin-right: -16px;
+  overflow: visible !important;
+  pointer-events: none;
+  box-shadow: none;
+}
+
+.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%);
+  align-items: center;
+  justify-content: center;
+}
+
+.is-collapsed .collapse-handle {
+  right: -8px;
+  border-left: 1px solid var(--el-border-color-light);
+}
+
+.collapse-handle:hover {
+  color: var(--el-color-primary);
+  background-color: var(--el-fill-color-light);
+}
+
+.truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 1 - 1
src/components/WellSelect/index.vue

@@ -159,7 +159,7 @@ onMounted(() => {
 
 <template>
   <div
-    class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-4 transition-all duration-300 ease-in-out overflow-visible"
+    class="dept-aside-container 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 class="inner-content flex flex-col gap-4 h-full w-full">

+ 8 - 7
src/components/ZmTable/ZmTableColumn.vue

@@ -51,6 +51,7 @@ const tableContext = inject(TableContextKey, {
 const innerSortOrder = ref<SortOrder | null>(props.defaultSortOrder ?? null)
 const filterVisible = ref(false)
 const settingVisible = ref(false)
+const hasHeaderAction = computed(() => props.action || props.zmSortable || props.zmFilterable)
 
 const defaultOptions = ref<Partial<Props>>({
   align: 'center',
@@ -74,10 +75,7 @@ const bindProps = computed(() => {
     ...columnProps
   } = props
   const columnAlign = props.align || (attrs.align as ColumnAlign | undefined)
-  const resolvedAlign =
-    action || zmSortable || zmFilterable
-      ? 'left'
-      : columnAlign || tableContext.columnAlign.value || defaultOptions.value.align
+  const resolvedAlign = columnAlign || tableContext.columnAlign.value || defaultOptions.value.align
 
   return {
     ...defaultOptions.value,
@@ -95,7 +93,10 @@ const alignMap: Record<string, string> = {
   left: 'justify-between',
   right: 'justify-end'
 }
-const headerFlexClass = computed(() => alignMap[String(bindProps.value.align)] || 'justify-center')
+const headerFlexClass = computed(() => {
+  if (hasHeaderAction.value) return 'justify-between'
+  return alignMap[String(bindProps.value.align)] || 'justify-center'
+})
 
 const isSortControlled = computed(() => props.sortOrder !== undefined)
 const currentOrder = computed<SortOrder | null>(() => {
@@ -212,7 +213,7 @@ const calculativeWidth = () => {
     .map((item) => props.realValue?.(item) ?? item[props.prop as keyof typeof item])
     .filter(hasFilterValue)
   let labelWidth = getTextWidth(bindProps.value.label || '') + 32
-  if (props.zmFilterable || props.zmSortable) labelWidth += 8
+  if (hasHeaderAction.value) labelWidth += 8
   if (props.zmFilterable) labelWidth += 22
   if (props.zmSortable) labelWidth += 22
   const maxWidth = Math.min(
@@ -242,7 +243,7 @@ watch(
       <slot name="header" v-bind="scope">
         <div class="header-wrapper" :class="headerFlexClass">
           <span class="truncate" :title="scope.column.label">{{ scope.column.label }}</span>
-          <div v-if="props.action || props.zmSortable || props.zmFilterable" class="action-area">
+          <div v-if="hasHeaderAction" class="action-area">
             <el-tooltip
               v-if="props.zmSortable"
               :content="

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

@@ -545,7 +545,7 @@ export default {
     name: '查询条件',
     nameHolder: '请输入查询条件',
     status: '工单状态',
-    createTime: '创建时间:',
+    createTime: '创建时间',
     start: '开始日期',
     end: '结束日期',
     search: '搜索',

+ 1 - 0
src/views/oli-connection/monitoring-query/index.vue

@@ -255,6 +255,7 @@ function formatterValue(row: ListItem) {
       :init-select="false"
       :show-title="false"
       @node-click="handleNodeClick"
+      class="row-span-2"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
 

+ 1 - 0
src/views/oli-connection/monitoring/index.vue

@@ -179,6 +179,7 @@ const openDetail = (
       v-model="query.deptId"
       :init-select="false"
       :show-title="false"
+      class="row-span-2"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form

+ 432 - 314
src/views/pms/device/allotlog/DeviceAllot.vue

@@ -1,267 +1,71 @@
-<template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item
-            :label="t('devicePerson.deviceCode')"
-            prop="deviceCode"
-            style="margin-left: 25px"
-          >
-            <el-input
-              v-model="queryParams.deviceCode"
-              :placeholder="t('devicePerson.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('devicePerson.deviceName')" prop="deviceName">
-            <el-input
-              v-model="queryParams.deviceName"
-              :placeholder="t('devicePerson.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('deviceStatus.transfer')" prop="setFlag">
-            <el-select
-              v-model="queryParams.setFlag"
-              :placeholder="t('deviceStatus.choose')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in resultOptions"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="创建时间">
-            <el-date-picker
-              v-model="queryParams.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-220px"
-            />
-          </el-form-item>
-          <el-form-item
-            v-show="ifShow"
-            :label="t('devicePerson.status')"
-            label-width="85px"
-            prop="deviceStatus"
-          >
-            <el-select
-              v-model="queryParams.deviceStatus"
-              :placeholder="t('devicePerson.status')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item v-show="ifShow" :label="t('devicePerson.assets')" prop="assetProperty">
-            <el-select
-              v-model="queryParams.assetProperty"
-              :placeholder="t('devicePerson.assets')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item v-show="ifShow" :label="t('devicePerson.brand')" prop="brand">
-            <el-input
-              v-model="queryParams.brand"
-              :placeholder="t('devicePerson.brandHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-
-          <el-form-item>
-            <el-button v-if="!ifShow" @click="moreQuery(true)" type="warning"
-              ><Icon icon="ep:search" class="mr-5px" />
-              {{ t('devicePerson.moreSearch') }}</el-button
-            >
-            <el-button v-if="ifShow" @click="moreQuery(false)" type="danger"
-              ><Icon icon="ep:search" class="mr-5px" />
-              {{ t('devicePerson.closeSearch') }}</el-button
-            >
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create', undefined, queryParams.deptId)"
-              v-hasPermi="['pms:iot-device-allot-log:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('deviceAllot.setUp') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-device:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap>
-        <el-table
-          v-loading="loading"
-          :data="list"
-          height="calc(85vh - 173px)"
-          :stripe="true"
-          :show-overflow-tooltip="true"
-        >
-          <el-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
-          <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName">
-            <template #default="scope">
-              <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
-                {{ scope.row.deviceName }}
-              </el-link>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('devicePerson.dept')" align="center" prop="deptName" />
-          <el-table-column :label="t('deviceStatus.status')" align="center" prop="deviceStatus">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
-            </template>
-          </el-table-column>
-          <!--
-          <el-table-column
-            label="创建时间"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180px"
-          /> -->
-          <el-table-column :label="t('deviceStatus.operation')" align="center" min-width="120px">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="handleView(scope.row.id)"
-                v-hasPermi="['rq:iot-device:query']"
-              >
-                {{ t('deviceAllot.adjustmentRecords') }}
-              </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <DeviceAllotLogDrawer
-    :model-value="drawerVisible"
-    @update:model-value="(val) => (drawerVisible = val)"
-    :device-id="currentDeviceId"
-    ref="showDrawer"
-  />
-</template>
-
 <script setup lang="ts">
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceAllotLogDrawer from '@/views/pms/device/allotlog/DeviceAllotLogDrawer.vue'
+import download from '@/utils/download'
 import { rangeShortcuts } from '@/utils/formatTime'
+import DeviceAllotLogDrawer from '@/views/pms/device/allotlog/DeviceAllotLogDrawer.vue'
 
-/** 设备调拨 列表 */
 defineOptions({ name: 'IotDeviceAllot' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由跳转
+interface QueryParams extends PageParam {
+  deptId?: number
+  deviceCode?: string
+  deviceName?: string
+  brand?: string
+  setFlag?: string
+  deviceStatus?: string
+  assetProperty?: string
+  createTime?: string[]
+}
 
-const loading = ref(true) // 列表的加载中
-const ifShow = ref(false)
-const isDetail = ref(false) // 是否查看详情
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-let isLeftContentCollapsed = ref(false)
-const queryParams = reactive({
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotDeviceVO>()
+
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
+  deptId: undefined,
   deviceCode: undefined,
   deviceName: undefined,
   brand: undefined,
-  model: undefined,
-  deptId: undefined,
+  setFlag: '',
   deviceStatus: undefined,
   assetProperty: undefined,
-  picUrl: undefined,
-  remark: undefined,
-  manufacturerId: undefined,
-  supplierId: undefined,
-  nameplate: undefined,
-  expires: undefined,
-  creator: undefined,
-  setFlag: '',
   createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-const contentSpan = ref(20)
-const treeShow = ref(true)
-const currentDeviceId = ref() // 设备id
-const drawerVisible = ref<boolean>(false)
+}
+
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const ifShow = ref(false)
+const list = ref<IotDeviceVO[]>([])
+const total = ref(0)
+const currentDeviceId = ref<number>()
+const drawerVisible = ref(false)
 const showDrawer = ref()
 
-/** 查询列表 */
+const resultOptions = computed(() => [
+  {
+    label: '全部',
+    value: 'A'
+  },
+  {
+    label: '是',
+    value: 'Y'
+  },
+  {
+    label: '否',
+    value: 'N'
+  }
+])
+
 const getList = async () => {
   loading.value = true
   try {
@@ -273,46 +77,38 @@ const getList = async () => {
   }
 }
 
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  await getList()
-}
-
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
-const moreQuery = (show) => {
-  ifShow.value = show
-}
-/** 重置按钮操作 */
+
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 查看设备调拨详情 */
-const handleView = async (deviceId: number) => {
-  currentDeviceId.value = deviceId
-  drawerVisible.value = true
-  // 强制重新加载数据
-  nextTick(() => {
-    showDrawer.value?.loadDeviceAllots(deviceId)
-  })
-  showDrawer.value.openDrawer()
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  //修改
-  if (typeof id === 'number') {
-    push({ name: 'DeviceDetailEdit', params: { id } })
-    return
-  }
-  // 新增
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeptNodeClick = async (row: Tree) => {
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
+const moreQuery = (show: boolean) => {
+  ifShow.value = show
+}
+
+const openForm = () => {
   push({
     name: 'ConfigDeviceAllot',
     query: {
@@ -321,59 +117,381 @@ const openForm = (type: string, id?: number) => {
   })
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotDeviceApi.deleteIotDevice(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-// 是否设置过责任人 下拉列表 模拟字典项
-const resultOptions = computed(() => [
-  {
-    label: '全部',
-    value: 'A' // 空值会触发 clearable 效果
-  },
-  {
-    label: '是',
-    value: 'Y' // 空值会触发 clearable 效果
-  },
-  {
-    label: '否',
-    value: 'N' // 空值会触发 clearable 效果
-  }
-])
-
 const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
 }
 
-const handleUpload = (id: number) => {
-  push({ name: 'DeviceUpload', params: { id } })
+const handleView = async (deviceId: number) => {
+  currentDeviceId.value = deviceId
+  drawerVisible.value = true
+  nextTick(() => {
+    showDrawer.value?.loadDeviceAllots(deviceId)
+  })
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
+  exportLoading.value = true
   try {
-    exportLoading.value = true
     const data = await IotDeviceApi.exportIotDeviceAllot(queryParams)
     download.excel(data, '设备调拨记录.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
-const { wsCache } = useCache()
-/** 初始化 **/
+
 onMounted(() => {
-  const user = wsCache.get(CACHE_KEY.USER)
   getList()
 })
 </script>
-<style scoped></style>
+
+<template>
+  <div
+    class="device-allot-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="device-allot-tree row-span-2"
+      @node-click="handleDeptNodeClick"
+    />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="device-allot-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('devicePerson.deviceCode')" prop="deviceCode">
+          <el-input
+            v-model="queryParams.deviceCode"
+            :placeholder="t('devicePerson.codeHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('devicePerson.deviceName')" prop="deviceName">
+          <el-input
+            v-model="queryParams.deviceName"
+            :placeholder="t('devicePerson.nameHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('deviceStatus.transfer')" prop="setFlag" label-width="90px">
+          <el-select
+            v-model="queryParams.setFlag"
+            :placeholder="t('deviceStatus.choose')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in resultOptions"
+              :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="结束日期"
+            :shortcuts="rangeShortcuts"
+            :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('devicePerson.status')"
+          prop="deviceStatus"
+          label-width="85px"
+        >
+          <el-select
+            v-model="queryParams.deviceStatus"
+            :placeholder="t('devicePerson.status')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-show="ifShow" :label="t('devicePerson.assets')" prop="assetProperty">
+          <el-select
+            v-model="queryParams.assetProperty"
+            :placeholder="t('devicePerson.assets')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-show="ifShow" :label="t('devicePerson.brand')" prop="brand">
+          <el-input
+            v-model="queryParams.brand"
+            :placeholder="t('devicePerson.brandHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+      </div>
+
+      <el-form-item class="query-actions">
+        <el-button v-if="!ifShow" type="warning" @click="moreQuery(true)">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.moreSearch') }}
+        </el-button>
+        <el-button v-else type="danger" @click="moreQuery(false)">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.closeSearch') }}
+        </el-button>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('devicePerson.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm"
+          v-hasPermi="['pms:iot-device-allot-log:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('deviceAllot.setUp') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-device: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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="deviceCode" :label="t('monitor.deviceCode')" fixed="left" />
+              <ZmTableColumn prop="deviceName" :label="t('monitor.deviceName')" fixed="left">
+                <template #default="{ row }">
+                  <el-link :underline="false" type="primary" @click="handleDetail(row.id)">
+                    {{ row.deviceName }}
+                  </el-link>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="deptName" :label="t('devicePerson.dept')" />
+              <ZmTableColumn prop="deviceStatus" :label="t('deviceStatus.status')">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="row.deviceStatus" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('deviceStatus.operation')" width="150" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="handleView(row.id)"
+                    v-hasPermi="['rq:iot-device:query']"
+                  >
+                    {{ t('deviceAllot.adjustmentRecords') }}
+                  </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>
+
+  <DeviceAllotLogDrawer
+    ref="showDrawer"
+    :model-value="drawerVisible"
+    :device-id="currentDeviceId"
+    @update:model-value="(val) => (drawerVisible = val)"
+  />
+</template>
+
+<style scoped>
+.device-allot-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) {
+  .device-allot-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .device-allot-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) {
+  .device-allot-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(.device-allot-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .device-allot-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>

+ 253 - 264
src/views/pms/device/personlog/DevicePerson.vue

@@ -1,220 +1,66 @@
-<template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item
-            :label="t('devicePerson.deviceCode')"
-            prop="deviceCode"
-            style="margin-left: 20px"
-          >
-            <el-input
-              v-model="queryParams.deviceCode"
-              :placeholder="t('devicePerson.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('devicePerson.deviceName')" prop="deviceName">
-            <el-input
-              v-model="queryParams.deviceName"
-              :placeholder="t('devicePerson.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-
-          <el-form-item
-            :label="t('devicePerson.responsiblePerson')"
-            prop="setFlag"
-            label-width="140px"
-          >
-            <el-select
-              v-model="queryParams.setFlag"
-              :placeholder="t('devicePerson.choose')"
-              clearable
-              class="!w-150px"
-            >
-              <el-option
-                v-for="dict in resultOptions"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('devicePerson.rp')" prop="nickname">
-            <el-input
-              v-model="queryParams.nickname"
-              :placeholder="t('devicePerson.nicknameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item label="创建时间">
-            <el-date-picker
-              v-model="queryParams.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-220px"
-            />
-          </el-form-item>
-
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create', undefined, queryParams.deptId)"
-              v-hasPermi="['pms:iot-device-person-log:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('devicePerson.setUp') }}
-            </el-button>
-            <!-- v-hasPermi="['rq:iot-device:export']" -->
-            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap>
-        <el-table
-          v-loading="loading"
-          :data="list"
-          :stripe="true"
-          height="calc(85vh - 175px)"
-          :show-overflow-tooltip="true"
-        >
-          <el-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
-          <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName">
-            <template #default="scope">
-              <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
-                {{ scope.row.deviceName }}
-              </el-link>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('devicePerson.dept')" align="center" prop="deptName" />
-          <el-table-column :label="t('devicePerson.rp')" align="center" prop="responsibleNames" />
-          <el-table-column :label="t('devicePerson.operation')" align="center" min-width="120px">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="handleView(scope.row.id)"
-                v-hasPermi="['rq:iot-device:query']"
-              >
-                {{ t('devicePerson.adjustmentRecords') }}
-              </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <DevicePersonLogDrawer
-    :model-value="drawerVisible"
-    @update:model-value="(val) => (drawerVisible = val)"
-    :device-id="currentDeviceId"
-    ref="showDrawer"
-  />
-</template>
-
 <script setup lang="ts">
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DevicePersonLogDrawer from '@/views/pms/device/personlog/DevicePersonLogDrawer.vue'
+import { useUserStore } from '@/store/modules/user'
+import download from '@/utils/download'
 import { rangeShortcuts } from '@/utils/formatTime'
+import DevicePersonLogDrawer from '@/views/pms/device/personlog/DevicePersonLogDrawer.vue'
 
-/** 设备台账 列表 */
 defineOptions({ name: 'IotDevicePerson' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由跳转
+type DevicePersonRow = IotDeviceVO & {
+  responsibleNames?: string
+}
+
+interface QueryParams extends PageParam {
+  deptId?: number
+  deviceCode?: string
+  deviceName?: string
+  setFlag?: string
+  nickname?: string
+  createTime?: string[]
+}
+
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<DevicePersonRow>()
 
-const loading = ref(true) // 列表的加载中
-const ifShow = ref(false)
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-let isLeftContentCollapsed = ref(false)
-const queryParams = reactive({
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
+  deptId: undefined,
   deviceCode: undefined,
   deviceName: undefined,
-  brand: undefined,
-  model: undefined,
-  deptId: undefined,
-  deviceStatus: undefined,
-  assetProperty: undefined,
-  picUrl: undefined,
-  remark: undefined,
-  manufacturerId: undefined,
-  supplierId: undefined,
-  manDate: [],
-  nameplate: undefined,
-  expires: undefined,
-  plPrice: undefined,
-  plDate: [],
-  plYear: undefined,
-  plStartDate: [],
-  plMonthed: undefined,
-  plAmounted: undefined,
-  remainAmount: undefined,
-  infoId: undefined,
-  infoType: undefined,
-  infoName: undefined,
-  infoRemark: undefined,
-  infoUrl: undefined,
-  templateJson: undefined,
-  creator: undefined,
   setFlag: '',
   nickname: '',
   createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-const contentSpan = ref(20)
-const treeShow = ref(true)
+}
+
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<DevicePersonRow[]>([])
+const total = ref(0)
+
+const resultOptions = computed(() => [
+  {
+    label: '全部',
+    value: 'A'
+  },
+  {
+    label: '是',
+    value: 'Y'
+  },
+  {
+    label: '否',
+    value: 'N'
+  }
+])
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
@@ -226,35 +72,34 @@ const getList = async () => {
   }
 }
 
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  await getList()
-}
-
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
-const moreQuery = (show) => {
-  ifShow.value = show
-}
-/** 重置按钮操作 */
+
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  //修改
-  if (typeof id === 'number') {
-    push({ name: 'DeviceDetailEdit', params: { id } })
-    return
-  }
-  // 新增 传递当前选中的部门ID
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeptNodeClick = async (row: Tree) => {
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
+const openForm = () => {
   push({
     name: 'ConfigDevicePerson',
     query: {
@@ -263,70 +108,214 @@ const openForm = (type: string, id?: number) => {
   })
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotDeviceApi.deleteIotDevice(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-// 是否设置过责任人 下拉列表 模拟字典项
-const resultOptions = computed(() => [
-  {
-    label: '全部',
-    value: 'A' // 空值会触发 clearable 效果
-  },
-  {
-    label: '是',
-    value: 'Y' // 空值会触发 clearable 效果
-  },
-  {
-    label: '否',
-    value: 'N' // 空值会触发 clearable 效果
-  }
-])
-
 const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
 }
 
-/** 查看设备责任人调整详情 */
-const currentDeviceId = ref() // 设备id
-const drawerVisible = ref<boolean>(false)
+const currentDeviceId = ref<number>()
+const drawerVisible = ref(false)
 const showDrawer = ref()
 
 const handleView = async (deviceId: number) => {
   currentDeviceId.value = deviceId
   drawerVisible.value = true
-  // 强制重新加载数据
   nextTick(() => {
     showDrawer.value?.loadDevicePersons(deviceId)
   })
-  showDrawer.value.openDrawer()
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
+  exportLoading.value = true
   try {
     const data = await IotDeviceApi.exportIotDevicePerson(queryParams)
     download.excel(data, '设备责任人.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
-const { wsCache } = useCache()
-/** 初始化 **/
+
 onMounted(() => {
-  const user = wsCache.get(CACHE_KEY.USER)
-  // queryParams.deptId = user.user.deptId
   getList()
 })
 </script>
-<style scoped></style>
+
+<template>
+  <div
+    class="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"
+      @node-click="handleDeptNodeClick"
+      class="row-span-2"
+    />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-3 gap-6 flex items-center justify-between flex-wrap min-w-0"
+    >
+      <div class="flex items-center gap-6 flex-wrap">
+        <el-form-item :label="t('devicePerson.deviceCode')" prop="deviceCode">
+          <el-input
+            v-model="queryParams.deviceCode"
+            :placeholder="t('devicePerson.codeHolder')"
+            clearable
+            class="!w-180px"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('devicePerson.deviceName')" prop="deviceName">
+          <el-input
+            v-model="queryParams.deviceName"
+            :placeholder="t('devicePerson.nameHolder')"
+            clearable
+            class="!w-180px"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item
+          :label="t('devicePerson.responsiblePerson')"
+          prop="setFlag"
+          label-width="120px"
+        >
+          <el-select
+            v-model="queryParams.setFlag"
+            :placeholder="t('devicePerson.choose')"
+            clearable
+            class="!w-140px"
+          >
+            <el-option
+              v-for="dict in resultOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('devicePerson.rp')" prop="nickname">
+          <el-input
+            v-model="queryParams.nickname"
+            :placeholder="t('devicePerson.nicknameHolder')"
+            clearable
+            class="!w-180px"
+            @keyup.enter="handleQuery"
+          />
+        </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="结束日期"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-220px"
+          />
+        </el-form-item>
+      </div>
+
+      <el-form-item>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('devicePerson.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm"
+          v-hasPermi="['pms:iot-device-person-log:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('devicePerson.setUp') }}
+        </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">
+      <div class="flex-1 relative min-h-0">
+        <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="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="deviceCode" :label="t('monitor.deviceCode')" fixed="left" />
+              <ZmTableColumn prop="deviceName" :label="t('monitor.deviceName')" fixed="left">
+                <template #default="{ row }">
+                  <el-link :underline="false" type="primary" @click="handleDetail(row.id)">
+                    {{ row.deviceName }}
+                  </el-link>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="deptName" :label="t('devicePerson.dept')" />
+              <ZmTableColumn prop="responsibleNames" :label="t('devicePerson.rp')" />
+              <ZmTableColumn :label="t('devicePerson.operation')" width="150" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="handleView(row.id)"
+                    v-hasPermi="['rq:iot-device:query']"
+                  >
+                    {{ t('devicePerson.adjustmentRecords') }}
+                  </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>
+
+  <DevicePersonLogDrawer
+    ref="showDrawer"
+    :model-value="drawerVisible"
+    :device-id="currentDeviceId"
+    @update:model-value="(val) => (drawerVisible = val)"
+  />
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>

+ 428 - 324
src/views/pms/device/statuslog/DeviceStatus.vue

@@ -1,342 +1,114 @@
-<template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item
-            :label="t('devicePerson.deviceCode')"
-            prop="deviceCode"
-            style="margin-left: 25px"
-          >
-            <el-input
-              v-model="queryParams.deviceCode"
-              :placeholder="t('devicePerson.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('devicePerson.deviceName')" prop="deviceName">
-            <el-input
-              v-model="queryParams.deviceName"
-              :placeholder="t('devicePerson.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('deviceStatus.statusAdjust')" prop="setFlag" label-width="90px">
-            <el-select
-              v-model="queryParams.setFlag"
-              :label="t('deviceStatus.statusAdjust')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in resultOptions"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item
-            v-show="ifShow"
-            :label="t('devicePerson.status')"
-            label-width="85px"
-            prop="deviceStatus"
-          >
-            <el-select
-              v-model="queryParams.deviceStatus"
-              :label="t('devicePerson.status')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item v-show="ifShow" :label="t('devicePerson.assets')" prop="assetProperty">
-            <el-select
-              v-model="queryParams.assetProperty"
-              :placeholder="t('devicePerson.assetsHolder')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="创建时间">
-            <el-date-picker
-              v-model="queryParams.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-220px"
-            />
-          </el-form-item>
-
-          <el-form-item v-show="ifShow" :label="t('devicePerson.brand')" prop="brand">
-            <el-input
-              v-model="queryParams.brand"
-              :placeholder="t('devicePerson.brandHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-
-          <el-form-item>
-            <el-button v-if="!ifShow" @click="moreQuery(true)" type="warning"
-              ><Icon icon="ep:search" class="mr-5px" />
-              {{ t('devicePerson.moreSearch') }}</el-button
-            >
-            <el-button v-if="ifShow" @click="moreQuery(false)" type="danger"
-              ><Icon icon="ep:search" class="mr-5px" />
-              {{ t('devicePerson.closeSearch') }}</el-button
-            >
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create', undefined, queryParams.deptId)"
-              v-hasPermi="['pms:iot-device-status-log:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('deviceStatus.setUp') }}
-            </el-button>
-            <!-- v-hasPermi="['rq:iot-device:export']" -->
-            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap>
-        <el-table
-          height="calc(85vh - 175px)"
-          v-loading="loading"
-          :data="list"
-          :stripe="true"
-          :show-overflow-tooltip="true"
-        >
-          <el-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
-          <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName">
-            <template #default="scope">
-              <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
-                {{ scope.row.deviceName }}
-              </el-link>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('devicePerson.dept')" align="center" prop="deptName" />
-          <el-table-column :label="t('deviceStatus.status')" align="center" prop="deviceStatus">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
-            </template>
-          </el-table-column>
-          <!--
-          <el-table-column
-            label="创建时间"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180px"
-          /> -->
-          <el-table-column :label="t('deviceStatus.operation')" align="center" min-width="120px">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="handleView(scope.row.id)"
-                v-hasPermi="['rq:iot-device:query']"
-              >
-                {{ t('deviceStatus.adjustmentRecords') }}
-              </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <DeviceStatusLogDrawer
-    :model-value="drawerVisible"
-    @update:model-value="(val) => (drawerVisible = val)"
-    :device-id="currentDeviceId"
-    ref="showDrawer"
-  />
-</template>
-
 <script setup lang="ts">
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceStatusLogDrawer from '@/views/pms/device/statuslog/DeviceStatusLogDrawer.vue'
+import download from '@/utils/download'
 import { rangeShortcuts } from '@/utils/formatTime'
+import DeviceStatusLogDrawer from '@/views/pms/device/statuslog/DeviceStatusLogDrawer.vue'
 
-/** 设备台账 列表 */
 defineOptions({ name: 'IotDeviceStatus' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由跳转
-const drawerVisible = ref<boolean>(false)
-const loading = ref(true) // 列表的加载中
-const ifShow = ref(false)
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const currentDeviceId = ref() // 设备id
-let isLeftContentCollapsed = ref(false)
-const queryParams = reactive({
+interface QueryParams extends PageParam {
+  deptId?: number
+  deviceCode?: string
+  deviceName?: string
+  brand?: string
+  setFlag?: string
+  deviceStatus?: string
+  assetProperty?: string
+  createTime?: string[]
+}
+
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotDeviceVO>()
+
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
+  deptId: undefined,
   deviceCode: undefined,
   deviceName: undefined,
   brand: undefined,
-  model: undefined,
-  deptId: undefined,
+  setFlag: '',
   deviceStatus: undefined,
   assetProperty: undefined,
-  picUrl: undefined,
-  remark: undefined,
-  manufacturerId: undefined,
-  supplierId: undefined,
-  manDate: [],
-  nameplate: undefined,
-  expires: undefined,
-  plPrice: undefined,
-  plDate: [],
-  plYear: undefined,
-  plStartDate: [],
-  plMonthed: undefined,
-  plAmounted: undefined,
-  remainAmount: undefined,
-  infoId: undefined,
-  infoType: undefined,
-  infoName: undefined,
-  infoRemark: undefined,
-  infoUrl: undefined,
-  templateJson: undefined,
-  creator: undefined,
-  setFlag: '',
   createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-const contentSpan = ref(20)
-const treeShow = ref(true)
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await IotDeviceApi.statusRelationDevices(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
 }
 
-// 是否设置过责任人 下拉列表 模拟字典项
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const ifShow = ref(false)
+const list = ref<IotDeviceVO[]>([])
+const total = ref(0)
+const currentDeviceId = ref<number>()
+const drawerVisible = ref(false)
+const showDrawer = ref()
+
 const resultOptions = computed(() => [
   {
     label: '全部',
-    value: 'A' // 空值会触发 clearable 效果
+    value: 'A'
   },
   {
     label: '是',
-    value: 'Y' // 空值会触发 clearable 效果
+    value: 'Y'
   },
   {
     label: '否',
-    value: 'N' // 空值会触发 clearable 效果
+    value: 'N'
   }
 ])
 
-const showDrawer = ref()
-
-/** 查看设备状态调整详情 */
-const handleView = async (deviceId: number) => {
-  currentDeviceId.value = deviceId
-  drawerVisible.value = true
-  // 强制重新加载数据
-  nextTick(() => {
-    showDrawer.value?.loadDeviceStatuses(deviceId)
-  })
-  showDrawer.value.openDrawer()
-}
-
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  await getList()
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotDeviceApi.statusRelationDevices(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
-const moreQuery = (show) => {
-  ifShow.value = show
-}
-/** 重置按钮操作 */
+
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  //修改
-  if (typeof id === 'number') {
-    push({ name: 'DeviceDetailEdit', params: { id } })
-    return
-  }
-  // 新增
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeptNodeClick = async (row: Tree) => {
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
+const moreQuery = (show: boolean) => {
+  ifShow.value = show
+}
+
+const openForm = () => {
   push({
     name: 'ConfigDeviceStatus',
     query: {
@@ -345,43 +117,375 @@ const openForm = (type: string, id?: number) => {
   })
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotDeviceApi.deleteIotDevice(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
 const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
 }
 
-/** 导出按钮操作 */
+const handleView = async (deviceId: number) => {
+  currentDeviceId.value = deviceId
+  drawerVisible.value = true
+  nextTick(() => {
+    showDrawer.value?.loadDeviceStatuses(deviceId)
+  })
+}
+
 const handleExport = async () => {
+  exportLoading.value = true
   try {
-    // // 导出的二次确认
-    // await message.exportConfirm()
-    // // 发起导出
-    exportLoading.value = true
     const data = await IotDeviceApi.exportIotDeviceAdjust(queryParams)
     download.excel(data, '设备状态调整.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
-const { wsCache } = useCache()
-/** 初始化 **/
+
 onMounted(() => {
-  const user = wsCache.get(CACHE_KEY.USER)
-  // queryParams.deptId = user.user.deptId
   getList()
 })
 </script>
-<style scoped></style>
+
+<template>
+  <div
+    class="device-status-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="device-status-tree row-span-2"
+      @node-click="handleDeptNodeClick"
+    />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="device-status-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('devicePerson.deviceCode')" prop="deviceCode">
+          <el-input
+            v-model="queryParams.deviceCode"
+            :placeholder="t('devicePerson.codeHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('devicePerson.deviceName')" prop="deviceName">
+          <el-input
+            v-model="queryParams.deviceName"
+            :placeholder="t('devicePerson.nameHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('deviceStatus.statusAdjust')" prop="setFlag" label-width="100px">
+          <el-select
+            v-model="queryParams.setFlag"
+            :placeholder="t('deviceStatus.choose')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in resultOptions"
+              :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="结束日期"
+            :shortcuts="rangeShortcuts"
+            :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('devicePerson.status')"
+          prop="deviceStatus"
+          label-width="85px"
+        >
+          <el-select
+            v-model="queryParams.deviceStatus"
+            :placeholder="t('devicePerson.status')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-show="ifShow" :label="t('devicePerson.assets')" prop="assetProperty">
+          <el-select
+            v-model="queryParams.assetProperty"
+            :placeholder="t('devicePerson.assetsHolder')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-show="ifShow" :label="t('devicePerson.brand')" prop="brand">
+          <el-input
+            v-model="queryParams.brand"
+            :placeholder="t('devicePerson.brandHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+      </div>
+
+      <el-form-item class="query-actions">
+        <el-button v-if="!ifShow" type="warning" @click="moreQuery(true)">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.moreSearch') }}
+        </el-button>
+        <el-button v-else type="danger" @click="moreQuery(false)">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.closeSearch') }}
+        </el-button>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('devicePerson.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('devicePerson.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm"
+          v-hasPermi="['pms:iot-device-status-log:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('deviceStatus.setUp') }}
+        </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">
+      <div class="flex-1 relative min-h-0">
+        <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="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="deviceCode" :label="t('monitor.deviceCode')" fixed="left" />
+              <ZmTableColumn prop="deviceName" :label="t('monitor.deviceName')" fixed="left">
+                <template #default="{ row }">
+                  <el-link :underline="false" type="primary" @click="handleDetail(row.id)">
+                    {{ row.deviceName }}
+                  </el-link>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="deptName" :label="t('devicePerson.dept')" />
+              <ZmTableColumn prop="deviceStatus" :label="t('deviceStatus.status')">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="row.deviceStatus" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('deviceStatus.operation')" width="150" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="handleView(row.id)"
+                    v-hasPermi="['rq:iot-device:query']"
+                  >
+                    {{ t('deviceStatus.adjustmentRecords') }}
+                  </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>
+
+  <DeviceStatusLogDrawer
+    ref="showDrawer"
+    :model-value="drawerVisible"
+    :device-id="currentDeviceId"
+    @update:model-value="(val) => (drawerVisible = val)"
+  />
+</template>
+
+<style scoped>
+.device-status-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) {
+  .device-status-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .device-status-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) {
+  .device-status-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(.device-status-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .device-status-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>

+ 279 - 227
src/views/pms/devicetemplate/index.vue

@@ -1,202 +1,40 @@
-<template>
-  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
-  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
-  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
-
-  <el-row :gutter="20">
-    <!-- 左侧 设备分类 树 -->
-    <el-col :span="4" :xs="24">
-      <ContentWrap class="h-1/1">
-        <DeviceCategoryTree @node-click="handleDeviceCategoryTreeNodeClick" />
-      </ContentWrap>
-    </el-col>
-    <el-col :span="20" :xs="24">
-      <!-- 搜索 -->
-      <ContentWrap>
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item label="模板名称" prop="name">
-            <el-input
-              v-model="queryParams.name"
-              placeholder="请输入模板名称"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item label="模板编码" prop="code">
-            <el-input
-              v-model="queryParams.code"
-              placeholder="请输入模板编码"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item label="状态" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              placeholder="属性模板状态"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_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="datetimerange"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
-            <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-device-template:create']"
-            >
-              <Icon icon="ep:plus" /> 新增
-            </el-button>
-            <!--
-            <el-button @click="handleAllQuery"><Icon icon="ep:search" />查询所有</el-button> -->
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-      <ContentWrap>
-        <el-table v-loading="loading" :data="list">
-          <el-table-column label="序号" width="60" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <el-table-column
-            label="模板编码"
-            align="center"
-            prop="code"
-            :show-overflow-tooltip="true"
-          />
-          <el-table-column
-            label="模板名称"
-            align="center"
-            prop="name"
-            :show-overflow-tooltip="true"
-          />
-          <el-table-column
-            label="设备分类名称"
-            align="center"
-            key="deviceCategoryName"
-            prop="deviceCategoryName"
-            :show-overflow-tooltip="true"
-          />
-          <el-table-column label="状态" key="status">
-            <template #default="scope">
-              <el-switch
-                v-model="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="handleStatusChange(scope.row)"
-                :disabled="!checkPermi(['rq:iot-device-template:update'])"
-              />
-            </template>
-          </el-table-column>
-          <el-table-column
-            label="创建时间"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180"
-          />
-          <el-table-column label="操作" align="center" width="160">
-            <template #default="scope">
-              <div class="flex items-center justify-center">
-                <el-button
-                  type="primary"
-                  link
-                  @click="openDetail(scope.row)"
-                >
-                  <Icon icon="ep:edit" />查看
-                </el-button>
-                <el-button
-                  type="primary"
-                  link
-                  @click="openForm('update', scope.row.id)"
-                  v-hasPermi="['rq:iot-device-template:update']"
-                >
-                  <Icon icon="ep:edit" />修改
-                </el-button>
-                <el-dropdown
-                  @command="(command) => handleCommand(command, scope.row)"
-                  v-hasPermi="[
-                    'rq:iot-device-template:delete'
-                  ]"
-                >
-                  <el-button type="primary" link><Icon icon="ep:d-arrow-right" /> 更多</el-button>
-                  <template #dropdown>
-                    <el-dropdown-menu>
-                      <el-dropdown-item
-                        command="handleDelete"
-                        v-if="checkPermi(['rq:iot-device-template:delete'])"
-                      >
-                        <Icon icon="ep:delete" />删除
-                      </el-dropdown-item>
-                    </el-dropdown-menu>
-                  </template>
-                </el-dropdown>
-              </div>
-            </template>
-          </el-table-column>
-        </el-table>
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-
-  <!-- 添加或修改 属性模板 对话框 -->
-  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
-</template>
 <script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { checkPermi } from '@/utils/permission'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, rangeShortcuts } from '@/utils/formatTime'
 import { CommonStatusEnum } from '@/utils/constants'
 import * as DeviceTemplateApi from '@/api/pms/devicetemplate'
+import DeviceCategoryTree from '@/components/DeviceCategoryTree/index.vue'
 import TemplateForm from './TemplateForm.vue'
-import DeviceCategoryTree from './DeviceCategoryTree.vue'
-import { useTreeStore } from '@/store/modules/attrTemplateTreeStore';
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
 
 defineOptions({ name: 'DeviceAttrsTemplate' })
 
-const treeStore = useTreeStore();
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type DeviceTemplateRow = Omit<DeviceTemplateApi.DeviceAttrTemplateVO, 'status'> & {
+  status?: number
+  deviceCategoryName?: string
+  createTime?: string
+}
+
+interface QueryParams extends PageParam {
+  name?: string
+  code?: string
+  status?: number
+  deviceCategoryId?: number
+  createTime?: string[]
+}
 
+const treeStore = useTreeStore()
+const message = useMessage()
+const { t } = useI18n()
 const { push } = useRouter()
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数
-const queryParams = reactive({
+const { ZmTable, ZmTableColumn } = useTableComponents<DeviceTemplateRow>()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<DeviceTemplateRow[]>([])
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   name: undefined,
@@ -205,110 +43,324 @@ const queryParams = reactive({
   deviceCategoryId: undefined,
   createTime: []
 })
-const queryFormRef = ref() // 搜索的表单
+const queryFormRef = ref()
+
+const selectedId = computed(() => treeStore.selectedId)
 
-// 从 Store 中获取左侧 设备分类树 选中的 节点ID
-const selectedId = computed(() => treeStore.selectedId);
+const normalizeStatus = (status: unknown) => {
+  const value = Number(status)
+  if (value === CommonStatusEnum.ENABLE || value === CommonStatusEnum.DISABLE) {
+    return value
+  }
+  return undefined
+}
+
+const isValidStatus = (status: unknown) => normalizeStatus(status) !== undefined
 
-/** 查询 设备属性模板 列表 */
 const getList = async () => {
   loading.value = true
   try {
     const data = await DeviceTemplateApi.getDeviceTemplatePage(queryParams)
-    list.value = data.list
+    list.value = data.list.map((item: DeviceTemplateRow) => ({
+      ...item,
+      status: normalizeStatus(item.status)
+    }))
     total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 搜索按钮操作 */
-const handleAllQuery = () => {
-  queryParams.pageNo = 1
-  queryParams.deviceCategoryId = ''
-  getList()
-}
-
-/** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 处理 设备分类树 被点击 */
-const handleDeviceCategoryTreeNodeClick = async (row) => {
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeviceCategoryTreeNodeClick = async (row: Tree) => {
+  treeStore.setSelectedId(row.id)
   queryParams.deviceCategoryId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 
-/** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
-/** 打开详情 */
-const openDetail = (row) => {
+const openDetail = (row: DeviceTemplateRow) => {
   push({
     name: 'DeviceAttrTemplateModel',
     params: {
       id: row.deviceCategoryId,
-      // 添加额外参数
       templateName: row.name,
       categoryName: row.deviceCategoryName
     }
   })
 }
 
-/** 修改 设备属性模板 状态 */
-const handleStatusChange = async (row: DeviceTemplateApi.DeviceAttrTemplateVO) => {
+const getStatusText = (status: unknown) => {
+  const value = normalizeStatus(status)
+  if (value === CommonStatusEnum.ENABLE) return '启用'
+  if (value === CommonStatusEnum.DISABLE) return '停用'
+  return '未知'
+}
+
+const handleStatusChange = async (
+  row: DeviceTemplateRow,
+  nextStatus: string | number | boolean
+) => {
+  const status = normalizeStatus(nextStatus)
+  const oldStatus = normalizeStatus(row.status)
+  if (
+    !row.id ||
+    !row.name ||
+    status === undefined ||
+    oldStatus === undefined ||
+    status === oldStatus
+  ) {
+    return
+  }
+
   try {
-    // 修改状态的二次确认
-    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '""' + row.name + '"属性模板吗?')
-    // 发起修改状态
-    await DeviceTemplateApi.updateDeviceTemplateStatus(row.id, row.status)
-    // 刷新列表
+    await message.confirm(`确认要${getStatusText(status)}"${row.name}"属性模板吗?`)
+    row.status = status
+    await DeviceTemplateApi.updateDeviceTemplateStatus(row.id, status)
     await getList()
   } catch {
-    // 取消后,进行恢复按钮
-    row.status =
-      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+    row.status = oldStatus
   }
 }
 
-/** 操作分发 */
-const handleCommand = (command: string, row: DeviceTemplateApi.DeviceAttrTemplateVO) => {
+const handleCommand = (command: string, row: DeviceTemplateRow) => {
   switch (command) {
     case 'handleDelete':
-      handleDelete(row.id)
+      handleDelete(row.id!)
       break
     default:
       break
   }
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
-    // 删除的二次确认
     await message.delConfirm()
-    // 发起删除
     await DeviceTemplateApi.deleteDeviceTemplate(id)
     message.success(t('common.delSuccess'))
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 初始化 */
 onMounted(() => {
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="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))]"
+  >
+    <DeviceCategoryTree
+      v-model="queryParams.deviceCategoryId"
+      :show-title="false"
+      @node-click="handleDeviceCategoryTreeNodeClick"
+      class="row-span-2"
+    />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-3 gap-6 flex items-center justify-between flex-wrap min-w-0"
+    >
+      <div class="flex items-center gap-6 flex-wrap">
+        <el-form-item label="模板名称" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="请输入模板名称"
+            clearable
+            class="!w-200px"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="模板编码" prop="code">
+          <el-input
+            v-model="queryParams.code"
+            placeholder="请输入模板编码"
+            clearable
+            class="!w-200px"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            placeholder="请选择状态"
+            clearable
+            class="!w-180px"
+          >
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_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="datetimerange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-320px"
+          />
+        </el-form-item>
+      </div>
+
+      <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-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['rq:iot-device-template:create']"
+        >
+          <Icon icon="ep:plus" 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">
+      <div class="flex-1 relative min-h-0">
+        <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="60"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="code" label="模板编码" fixed="left" />
+              <ZmTableColumn prop="name" label="模板名称" fixed="left" />
+              <ZmTableColumn prop="deviceCategoryName" label="设备分类名称" />
+              <ZmTableColumn
+                prop="status"
+                label="状态"
+                :real-value="(row) => getStatusText(row.status)"
+                :width="90"
+              >
+                <template #default="{ row }">
+                  <el-switch
+                    v-if="isValidStatus(row.status)"
+                    :model-value="row.status"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="!checkPermi(['rq:iot-device-template:update'])"
+                    @change="(status) => handleStatusChange(row, status)"
+                  />
+                  <el-tag v-else type="info">未知</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="createTime"
+                label="创建时间"
+                :formatter="dateFormatter"
+                width="180"
+              />
+              <ZmTableColumn label="操作" width="220" fixed="right" action>
+                <template #default="{ row }">
+                  <div class="flex items-center justify-center gap-2">
+                    <el-button type="primary" link @click="openDetail(row)">
+                      <Icon icon="ep:view" class="mr-3px" />查看
+                    </el-button>
+                    <el-button
+                      type="primary"
+                      link
+                      @click="openForm('update', row.id)"
+                      v-hasPermi="['rq:iot-device-template:update']"
+                    >
+                      <Icon icon="ep:edit" class="mr-3px" />修改
+                    </el-button>
+                    <el-dropdown
+                      @command="(command) => handleCommand(command, row)"
+                      v-hasPermi="['rq:iot-device-template:delete']"
+                    >
+                      <el-button type="primary" link>
+                        <Icon icon="ep:more-filled" class="mr-3px" />更多
+                      </el-button>
+                      <template #dropdown>
+                        <el-dropdown-menu>
+                          <el-dropdown-item
+                            command="handleDelete"
+                            v-if="checkPermi(['rq:iot-device-template:delete'])"
+                          >
+                            <Icon icon="ep:delete" class="mr-5px" />删除
+                          </el-dropdown-item>
+                        </el-dropdown-menu>
+                      </template>
+                    </el-dropdown>
+                  </div>
+                </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>
+
+  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>

+ 459 - 535
src/views/pms/iotlockstock/index.vue

@@ -1,230 +1,52 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item
-        :label="t('workOrderMaterial.factory')"
-        prop="factoryId"
-        v-if="!shouldHideComponents"
-      >
-        <el-select
-          v-model="queryParams.factoryId"
-          clearable
-          filterable
-          :placeholder="t('faultForm.choose')"
-          class="!w-240px"
-          @change="selectedFactoryChange"
-        >
-          <el-option
-            v-for="item in factoryList"
-            :key="item.id"
-            :label="item.factoryName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        :label="t('workOrderMaterial.costCenter')"
-        prop="costCenterId"
-        v-if="!shouldHideComponents"
-      >
-        <el-select
-          v-model="queryParams.costCenterId"
-          clearable
-          filterable
-          :placeholder="t('faultForm.choose')"
-          class="!w-240px"
-        >
-          <el-option
-            v-for="item in filteredCostCenterList"
-            :key="item.id"
-            :label="item.costCenterName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
-        <el-input
-          v-model="queryParams.materialCode"
-          :placeholder="t('chooseMaintain.materialCode')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
-        <el-input
-          v-model="queryParams.materialName"
-          :placeholder="t('chooseMaintain.materialName')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.createTime')" prop="storageTime">
-        <el-date-picker
-          v-model="queryParams.storageTime"
-          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="['pms:iot-lock-stock:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
-        </el-button>
-        <!-- v-hasPermi="['pms:iot-lock-stock:export']" -->
-        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- ========== 统计信息卡片 ========== -->
-  <ContentWrap style="margin-bottom: 16px">
-    <el-card shadow="never" class="stat-card">
-      <div class="stat-container">
-        <div class="stat-item">
-          <span class="stat-label">{{ t('stock.totalQuantity') }}:</span>
-          <span class="stat-value">{{ totalQuantity.toLocaleString() }}</span>
-        </div>
-        <div class="stat-item">
-          <span class="stat-label">{{ t('stock.totalAmount') }}:</span>
-          <span class="stat-value"
-            >¥
-            {{
-              totalAmount.toLocaleString(undefined, {
-                minimumFractionDigits: 2,
-                maximumFractionDigits: 2
-              })
-            }}</span
-          >
-        </div>
-      </div>
-    </el-card>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap ref="tableContainerRef" class="table-container">
-    <el-table
-      ref="tableRef"
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="false"
-      style="width: 100%"
-    >
-      <el-table-column
-        :label="t('workOrderMaterial.factory')"
-        align="center"
-        prop="factory"
-        :width="columnWidths.factory"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.costCenter')"
-        align="center"
-        prop="costCenter"
-        :width="columnWidths.costCenter"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.materialCode')"
-        align="center"
-        prop="materialCode"
-        :width="columnWidths.materialCode"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.materialName')"
-        align="left"
-        prop="materialName"
-        :width="columnWidths.materialName"
-      />
-      <el-table-column
-        :label="t('route.quantity')"
-        align="center"
-        prop="quantity"
-        :formatter="erpPriceTableColumnFormatter"
-        :width="columnWidths.quantity"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.unitPrice')"
-        align="center"
-        prop="unitPrice"
-        :formatter="erpPriceTableColumnFormatter"
-        :width="columnWidths.unitPrice"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.unit')"
-        align="center"
-        prop="unit"
-        :width="columnWidths.unit"
-      />
-      <el-table-column
-        :label="t('stock.storageTime')"
-        align="center"
-        prop="storageTime"
-        :formatter="dateFormatter"
-        :width="columnWidths.storageTime"
-      />
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <IotLockStockForm ref="formRef" @success="getList" />
-</template>
-
 <script setup lang="ts">
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotLockStockApi, IotLockStockVO } from '@/api/pms/iotlockstock'
-import IotLockStockForm from './IotLockStockForm.vue'
-import { erpPriceTableColumnFormatter } from '@/utils'
-import { useUserStore } from '@/store/modules/user'
 import { SapOrgApi, SapOrgVO } from '@/api/system/saporg'
+import { useUserStore } from '@/store/modules/user'
 import { checkRole } from '@/utils/permission'
-import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import CountTo from '@/components/count-to1.vue'
 
-/** PMS 本地 库存 列表 */
 defineOptions({ name: 'IotLockStock' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type LockStockRow = IotLockStockVO & {
+  totalQuantity?: number | string
+  totalAmount?: number | string
+  storageTime?: string
+}
 
-const { push } = useRouter() // 路由跳转
-// 过滤后的成本中心列表(用于动态显示)
-const filteredCostCenterList = ref<SapOrgVO[]>([])
-const loading = ref(true) // 列表的加载中
-const list = ref<IotLockStockVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+interface QueryParams extends PageParam {
+  deptId?: number
+  factory?: string
+  factoryId?: number
+  projectDepartment?: string
+  storageLocationId?: number
+  costCenter?: string
+  costCenterId?: number
+  pickingListNumber?: string
+  materialCode?: string
+  materialName?: string
+  materialGroupName?: string
+  materialGroupId?: number
+  quantity?: number
+  unitPrice?: number
+  unit?: string
+  storageTime?: string[]
+  sort?: number
+  status?: number
+  processInstanceId?: string
+  auditStatus?: number
+  remark?: string
+  createTime?: string[]
+}
+
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<LockStockRow>()
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -249,414 +71,516 @@ const queryParams = reactive({
   auditStatus: undefined,
   remark: undefined,
   createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-
-const factoryList = ref([] as SapOrgVO[]) // 工厂列表
-const storageLocationList = ref([] as SapOrgVO[]) // 库存地点列表
-const costCenterList = ref([] as SapOrgVO[]) // SAP成本中心列表
-
-// 统计变量
-const totalQuantity = ref(0) // 总数量
-const totalAmount = ref(0) // 总金额
-
-// 表格容器和表格的引用
-const tableContainerRef = ref()
-const tableRef = ref()
-
-const shouldHideComponents = computed(() => {
-  // 检查用户是否拥有 'A' 或 'B' 角色
-  return checkRole(['小队队长', '操作员'])
-})
+}
 
-const selectedFactoryReqVO = ref({
-  type: 0, // 类型(1工厂 2成本中心 3库位)
-  factoryCodes: [] // 已经选择的SAP工厂code 列表
-})
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<LockStockRow[]>([])
+const total = ref(0)
+const totalQuantity = ref(0)
+const totalAmount = ref(0)
+const factoryList = ref<SapOrgVO[]>([])
+const costCenterList = ref<SapOrgVO[]>([])
+const filteredCostCenterList = ref<SapOrgVO[]>([])
 
-// 列宽度配置
-const columnWidths = ref({
-  factory: '120px',
-  costCenter: '120px',
-  materialCode: '120px',
-  materialName: '200px', // 初始宽度,会被计算覆盖
-  quantity: '100px',
-  unitPrice: '100px',
-  unit: '100px',
-  storageTime: '180px'
-})
+const shouldHideComponents = computed(() => checkRole(['小队队长', '操作员']))
 
-/** 获取滚动条宽度 */
-const getScrollbarWidth = () => {
-  const outer = document.createElement('div')
-  outer.style.visibility = 'hidden'
-  outer.style.overflow = 'scroll'
-  document.body.appendChild(outer)
-
-  const inner = document.createElement('div')
-  outer.appendChild(inner)
-
-  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
-  outer.parentNode?.removeChild(outer)
-
-  return scrollbarWidth
-}
-
-/** 计算文本宽度 */
-const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span')
-  span.style.visibility = 'hidden'
-  span.style.position = 'absolute'
-  span.style.whiteSpace = 'nowrap'
-  span.style.fontSize = `${fontSize}px`
-  span.style.fontFamily = 'inherit'
-  span.innerText = text
-
-  document.body.appendChild(span)
-  const width = span.offsetWidth
-  document.body.removeChild(span)
-
-  return width
-}
-
-/** 计算列宽度 */
-const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80 // 最小列宽
-  const PADDING = 25 // 列内边距
-  const FLEXIBLE_COLUMN = 'materialName' // 可伸缩列
-  const scrollbarWidth = getScrollbarWidth() // 动态获取滚动条宽度
-
-  if (!tableContainerRef.value?.$el || list.value.length === 0) return
-
-  const containerWidth = tableContainerRef.value.$el.clientWidth
-
-  // 需要自适应的列配置
-  const autoColumns = [
-    { key: 'factory', label: t('workOrderMaterial.factory'), getValue: (row) => row.factory },
-    {
-      key: 'costCenter',
-      label: t('workOrderMaterial.costCenter'),
-      getValue: (row) => row.costCenter
-    },
-    {
-      key: 'materialCode',
-      label: t('chooseMaintain.materialCode'),
-      getValue: (row) => row.materialCode
-    },
-    {
-      key: 'materialName',
-      label: t('chooseMaintain.materialName'),
-      getValue: (row) => row.materialName
-    },
-    {
-      key: 'quantity',
-      label: t('route.quantity'),
-      getValue: (row) => erpPriceTableColumnFormatter(null, null, row.quantity, null)
-    },
-    {
-      key: 'unitPrice',
-      label: t('workOrderMaterial.unitPrice'),
-      getValue: (row) => erpPriceTableColumnFormatter(null, null, row.unitPrice, null)
-    },
-    { key: 'unit', label: t('workOrderMaterial.unit'), getValue: (row) => row.unit },
-    {
-      key: 'storageTime',
-      label: t('stock.storageTime'),
-      getValue: (row) => dateFormatter(null, null, row.storageTime)
-    }
-  ]
-
-  const newWidths: Record<string, string> = {}
-  let totalFixedWidth = 0 // 所有固定列的总宽度
-
-  // 计算除可伸缩列外的所有列宽度
-  autoColumns.forEach((col) => {
-    if (col.key === FLEXIBLE_COLUMN) return
-
-    const headerText = col.label
-    const headerWidth = getTextWidth(headerText) * 1.3 // 表头宽度(加粗效果增加20%)
-
-    // 计算内容最大宽度
-    let contentMaxWidth = 0
-    list.value.forEach((row) => {
-      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    // 取表头宽度、内容最大宽度和最小宽度的最大值
-    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-    newWidths[col.key] = `${finalWidth}px`
-    totalFixedWidth += finalWidth
-  })
-
-  // 处理可伸缩列(materialName)
-  const flexibleCol = autoColumns.find((col) => col.key === FLEXIBLE_COLUMN)
-  if (flexibleCol) {
-    const headerText = flexibleCol.label
-    const headerWidth = getTextWidth(headerText) * 1.3
-
-    let contentMaxWidth = 0
-    list.value.forEach((row) => {
-      const text = flexibleCol.getValue
-        ? String(flexibleCol.getValue(row))
-        : String(row[flexibleCol.key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-
-    // 剩余空间 = 容器宽度 - 其他列总宽度 - 垂直滚动条宽度(17px)
-    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth
-
-    // 可伸缩列的宽度取剩余空间和基础宽度的最大值
-    const flexibleWidth = Math.max(remainingWidth, baseWidth)
-    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`
+const stockStatCards = computed(() => [
+  {
+    label: t('stock.totalQuantity'),
+    value: totalQuantity.value,
+    prefix: '',
+    unit: '',
+    decimals: 0,
+    icon: 'i-tabler:packages text-sky'
+  },
+  {
+    label: t('stock.totalAmount'),
+    value: totalAmount.value,
+    prefix: '¥',
+    unit: '',
+    decimals: 2,
+    icon: 'i-material-symbols:payments-outline-rounded text-emerald'
   }
+])
 
-  // 更新列宽度
-  columnWidths.value = newWidths
-
-  // 重新布局表格
-  nextTick(() => {
-    tableRef.value?.doLayout()
-  })
-}
-
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotLockStockApi.getIotLockStockPage(queryParams)
     list.value = data.list
     total.value = data.total
-    // 从第一条记录中提取统计值
-    if (data.list && data.list.length > 0) {
-      // 确保取到有效的数值(第一条记录中的统计值代表整个查询结果)
-      totalQuantity.value = Number(data.list[0].totalQuantity) || 0
-      totalAmount.value = Number(data.list[0].totalAmount) || 0
-    } else {
-      // 没有数据时重置为0
-      totalQuantity.value = 0
-      totalAmount.value = 0
-    }
-    // 数据加载完成后计算列宽
-    nextTick(() => {
-      calculateColumnWidths()
-    })
+
+    const firstRow = data.list?.[0]
+    totalQuantity.value = Number(firstRow?.totalQuantity) || 0
+    totalAmount.value = Number(firstRow?.totalAmount) || 0
   } finally {
     loading.value = false
   }
-  // 获取当前登录人的部门id 查询部门及子部门关联的所有SAP组织
-  // 获取组织数据(移出统计值处理逻辑)
-  await loadOrgData()
 }
 
-// 单独封装组织数据加载方法
 const loadOrgData = async () => {
   const deptId = useUserStore().getUser.deptId
-  if (typeof deptId === 'number' && !isNaN(deptId) && deptId > 0) {
-    try {
-      const [factories, costCenters] = await Promise.all([
-        SapOrgApi.filteredSimpleSapOrgList(1, deptId),
-        SapOrgApi.filteredSimpleSapOrgList(2, deptId)
-      ])
-      factoryList.value = factories
-      costCenterList.value = costCenters
-      // 初始化时显示全部成本中心
-      filteredCostCenterList.value = costCenters
-    } catch (error) {
-      console.error('获取组织数据失败:', error)
-    }
-  } else {
-    console.warn('无效的部门ID:', deptId)
-  }
+  if (typeof deptId !== 'number' || Number.isNaN(deptId) || deptId <= 0) return
+
+  const [factories, costCenters] = await Promise.all([
+    SapOrgApi.filteredSimpleSapOrgList(1, deptId),
+    SapOrgApi.filteredSimpleSapOrgList(2, deptId)
+  ])
+  factoryList.value = factories
+  costCenterList.value = costCenters
+  filteredCostCenterList.value = costCenters
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  // 重置后恢复成本中心为完整列表
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   filteredCostCenterList.value = costCenterList.value
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  if (typeof id === 'number') {
-    formRef.value.open(type, id)
-    return
-  }
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const openForm = () => {
   push({ name: 'LockStockAdd', params: {} })
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotLockStockApi.deleteIotLockStock(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 已经选择了 SAP工厂 */
-/* const selectedFactoryChange = async (selectedId: number | undefined) => {
-
-  // 获取选中的factoryCode数组
-  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
-  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
-
-  // 获得已经选择的 SAP 工厂 数组
-  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 成本中心
-  selectedFactoryReqVO.value.type = 2
-  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
-  costCenterList.value = await SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
-
-  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
-  selectedFactoryReqVO.value.type = 3
-  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
-  storageLocationList.value = await SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
-} */
-
-/** 已经选择了 SAP工厂 */
-const selectedFactoryChange = async (selectedId: number | undefined) => {
-  // 清空已选择的成本中心
+const selectedFactoryChange = (selectedId: number | undefined) => {
   queryParams.costCenterId = undefined
 
   if (!selectedId) {
-    // 未选择工厂时显示全部成本中心
     filteredCostCenterList.value = costCenterList.value
     return
   }
 
-  // 获取选中的工厂对象
   const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
   if (!selectedFactory) return
 
-  // 根据工厂代码过滤成本中心
   filteredCostCenterList.value = costCenterList.value.filter(
     (item) => item.factoryCode === selectedFactory.factoryCode
   )
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
   try {
     exportLoading.value = true
     const data = await IotLockStockApi.exportIotLockStock(queryParams)
     download.excel(data, 'PMS 本地库存.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
-/** 初始化 **/
+const formatNumber = (value: unknown) => erpPriceTableColumnFormatter(null, null, value, null)
+
 onMounted(() => {
+  loadOrgData()
   getList()
-  // 添加窗口大小变化监听
-  window.addEventListener('resize', calculateColumnWidths)
 })
+</script>
 
-onUnmounted(() => {
-  // 移除窗口大小变化监听
-  window.removeEventListener('resize', calculateColumnWidths)
-})
+<template>
+  <div
+    class="iot-lock-stock-page grid grid-rows-[auto_auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+  >
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="82px"
+      class="iot-lock-stock-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0"
+    >
+      <div class="query-row">
+        <el-form-item
+          v-if="!shouldHideComponents"
+          :label="t('workOrderMaterial.factory')"
+          prop="factoryId"
+        >
+          <el-select
+            v-model="queryParams.factoryId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+            @change="selectedFactoryChange"
+          >
+            <el-option
+              v-for="item in factoryList"
+              :key="item.id"
+              :label="item.factoryName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="!shouldHideComponents"
+          :label="t('workOrderMaterial.costCenter')"
+          prop="costCenterId"
+        >
+          <el-select
+            v-model="queryParams.costCenterId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+          >
+            <el-option
+              v-for="item in filteredCostCenterList"
+              :key="item.id"
+              :label="item.costCenterName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
+          <el-input
+            v-model="queryParams.materialCode"
+            :placeholder="t('chooseMaintain.materialCode')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
+          <el-input
+            v-model="queryParams.materialName"
+            :placeholder="t('chooseMaintain.materialName')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.createTime')" prop="storageTime">
+          <el-date-picker
+            v-model="queryParams.storageTime"
+            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>
 
-// 监听列表数据变化,重新计算列宽
-watch(
-  list,
-  () => {
-    nextTick(calculateColumnWidths)
-  },
-  { deep: true }
-)
-</script>
+      <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"
+          v-hasPermi="['pms:iot-lock-stock:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </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="stock-stat-grid">
+      <div v-for="item in stockStatCards" :key="item.label" class="stock-stat-card group">
+        <div class="relative z-10 flex h-full flex-col justify-center">
+          <span class="stat-label">{{ item.label }}</span>
+          <div class="flex items-baseline gap-1">
+            <span v-if="item.prefix" class="text-[12px] text-gray-400">{{ item.prefix }}</span>
+            <count-to
+              class="stat-value"
+              :start-val="0"
+              :end-val="item.value"
+              :decimals="item.decimals"
+            />
+            <span v-if="item.unit" class="stat-unit">{{ item.unit }}</span>
+          </div>
+        </div>
+        <div class="stat-icon">
+          <div :class="item.icon" class="text-5xl"></div>
+        </div>
+      </div>
+    </div>
+
+    <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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn
+                prop="factory"
+                :label="t('workOrderMaterial.factory')"
+                min-width="120"
+              />
+              <ZmTableColumn
+                prop="costCenter"
+                :label="t('workOrderMaterial.costCenter')"
+                min-width="140"
+              />
+              <ZmTableColumn
+                prop="materialCode"
+                :label="t('chooseMaintain.materialCode')"
+                min-width="150"
+              />
+              <ZmTableColumn
+                prop="materialName"
+                :label="t('chooseMaintain.materialName')"
+                min-width="220"
+                align="left"
+              />
+              <ZmTableColumn prop="quantity" :label="t('route.quantity')" min-width="100">
+                <template #default="{ row }">
+                  {{ formatNumber(row.quantity) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="unitPrice"
+                :label="t('workOrderMaterial.unitPrice')"
+                min-width="100"
+              >
+                <template #default="{ row }">
+                  {{ formatNumber(row.unitPrice) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" min-width="90" />
+              <ZmTableColumn
+                prop="storageTime"
+                :label="t('stock.storageTime')"
+                min-width="180"
+                action
+              >
+                <template #default="{ row }">
+                  {{ dateFormatter(row, null, row.storageTime) }}
+                </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>
+</template>
 
 <style scoped>
-/* 统计卡片样式 */
-.stat-card {
-  border-radius: 4px;
-  border: 1px solid #ebeef5;
+.iot-lock-stock-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
 }
 
-.stat-container {
+.query-row {
   display: flex;
-  padding: 1px;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 22px;
+  min-width: 0;
 }
 
-.stat-item {
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
   display: flex;
-  align-items: center;
-  margin-right: 40px; /* 控制项间距 */
+  flex-wrap: wrap;
+  gap: 8px 10px;
+}
+
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+.query-control--date {
+  width: 220px;
 }
 
 .stat-label {
-  font-weight: bold;
-  color: #606266;
-  margin-right: 8px;
+  margin-bottom: 2px;
+  font-size: 11px;
+  font-weight: 500;
+  letter-spacing: 0;
+  color: var(--el-text-color-regular);
 }
 
 .stat-value {
+  font-family: var(--el-font-family);
   font-size: 18px;
-  font-weight: bold;
-  color: #409eff;
+  font-weight: 700;
+  line-height: 1;
+  color: var(--el-text-color-primary);
+}
+
+.stat-unit {
+  font-size: 10px;
+  font-weight: 400;
+  color: var(--el-text-color-secondary);
+}
+
+.stock-stat-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.stock-stat-card {
+  position: relative;
+  min-height: 78px;
+  padding: 10px 14px;
+  overflow: hidden;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-extra-light);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-lighter);
+  transition:
+    border-color 0.3s,
+    box-shadow 0.3s;
+}
+
+.stock-stat-card:hover {
+  border-color: var(--el-color-primary-light-7);
+  box-shadow: var(--el-box-shadow-light);
 }
 
-/* 表格容器样式 - 确保可以水平滚动 */
-.table-container {
-  overflow-x: auto;
+.stat-icon {
+  position: absolute;
+  right: -8px;
+  bottom: -12px;
+  opacity: 0.4;
+  transform: rotate(-10deg);
+  transition:
+    transform 0.5s,
+    opacity 0.5s;
 }
 
-/* 防止表格内容换行 */
-:deep(.el-table) .cell {
-  white-space: nowrap !important;
-  overflow: visible !important;
-  text-overflow: unset !important;
+.stock-stat-card:hover .stat-icon {
+  opacity: 0.55;
+  transform: scale(1.08) rotate(0);
 }
 
-/* 确保表格行不换行 */
-:deep(.el-table__row) {
-  white-space: nowrap;
+:deep(.el-form-item) {
+  margin-bottom: 0;
 }
 
-/* 防止表格内容换行 */
-:deep(.el-table .cell) {
-  white-space: nowrap !important;
+@media (width >= 2400px) {
+  .iot-lock-stock-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
 }
 
-/* 表头特别处理 */
-:deep(.el-table__header) {
-  .cell {
-    display: inline-block;
-    white-space: nowrap;
-    width: auto !important;
+@media (width <= 1500px) {
+  .iot-lock-stock-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 176px;
+  }
+
+  .query-control--date {
+    width: 210px;
   }
 }
 
-/* 表格整体布局优化 */
-:deep(.el-table__inner-wrapper) {
-  min-width: 100% !important;
-  width: auto !important;
+@media (width <= 1200px) {
+  .iot-lock-stock-page {
+    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)
+    );
+  }
+
+  .query-actions {
+    width: 100%;
+  }
 }
 
-/* 单元格内容完全显示 */
-:deep(.el-table__body-wrapper) .el-table__cell .cell {
-  display: block;
-  overflow: visible;
-  text-overflow: unset;
+@media (width <= 768px) {
+  .iot-lock-stock-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .stock-stat-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .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>

+ 401 - 239
src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue

@@ -1,200 +1,68 @@
-<template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('iotDevice.code')" prop="deviceCode" style="margin-left: 25px">
-            <el-input
-              v-model="queryParams.deviceCode"
-              :placeholder="t('iotDevice.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('iotDevice.name')" prop="deviceName">
-            <el-input
-              v-model="queryParams.deviceName"
-              :placeholder="t('iotDevice.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-200px"
-            />
-          </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="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-device:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap>
-        <el-table
-          height="calc(85vh - 135px)"
-          v-loading="loading"
-          :data="list"
-          :stripe="true"
-          :show-overflow-tooltip="true"
-        >
-          <el-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </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">
-            <template #default="scope">
-              <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
-                {{ scope.row.deviceName }}
-              </el-link>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('bomList.serviceDue')" align="center">
-            <template #default="scope">
-              <template v-if="hasMaintenancePlan(scope.row.mainDistance)">
-                <span :class="getDistanceClass(scope.row.mainDistance)">
-                  {{ scope.row.mainDistance }}
-                </span>
-              </template>
-              <span v-else>无保养计划</span>
-            </template>
-          </el-table-column>
-          <el-table-column
-            v-if="showRunTime"
-            label="累计运行时长H"
-            align="center"
-            prop="totalRunTime"
-          />
-          <el-table-column
-            v-if="showMileage"
-            label="累计运行里程KM"
-            align="center"
-            prop="totalMileage"
-          />
-          <el-table-column v-if="showMultiAttrs" label="多属性累计值" align="center" width="200">
-            <template #default="scope">
-              <template v-for="(value, key) in scope.row.multiAttrsTotalRuntime" :key="key">
-                <span v-if="value && key"> {{ key }}: {{ value }}&nbsp;&nbsp;</span>
-              </template>
-              <template v-for="(value, key) in scope.row.multiAttrsTotalMileage" :key="key">
-                <span v-if="value && key"> {{ key }}: {{ value }}&nbsp;&nbsp;</span>
-              </template>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" />
-          <el-table-column :label="t('devicePerson.rp')" align="center" prop="responsibleNames" />
-          <el-table-column :label="t('monitor.status')" align="center" prop="deviceStatus">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
-            </template>
-          </el-table-column>
-          <!-- 工单状态列 -->
-          <el-table-column :label="t('operationFill.status')" align="center">
-            <template #default="scope">
-              <span v-if="scope.row.shouldWorkOrder && scope.row.runningWorkOrder">
-                {{ t('mainPlan.generatedNotExecuted') }}
-              </span>
-              <span v-else-if="scope.row.shouldWorkOrder && !scope.row.runningWorkOrder">
-                {{ t('mainPlan.notGenerated') }}
-              </span>
-              <span v-else>-</span>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('monitor.operation')" align="center" min-width="60px">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openBomForm(scope.row)"
-                v-hasPermi="['rq:iot-device:query']"
-                v-if="hasMaintenancePlan(scope.row.mainDistance)"
-              >
-                {{ t('monitor.details') }}
-              </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <DeviceAlarmBomList ref="modelFormRef" :flag="flag" />
-</template>
-
 <script setup lang="ts">
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { IotMainWorkOrderApi } from '@/api/pms/iotmainworkorder'
+import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { useCache } from '@/hooks/web/useCache'
+import download from '@/utils/download'
 import DeviceAlarmBomList from '@/views/pms/iotmainworkorder/DeviceAlarmBomList.vue'
-let isLeftContentCollapsed = ref(false)
-const showRunTime = computed(() => {
-  const all = list.value?.map((item) => item.totalRunTime)
-  return all.some((item) => Boolean(item))
-})
 
-const showMileage = computed(() => {
-  const all = list.value?.map((item) => item.totalMileage)
-  return all.some((item) => Boolean(item))
-})
+defineOptions({ name: 'IotDeviceMainAlarm' })
 
-const showMultiAttrs = computed(() => {
-  const all = list.value
-    ?.map((item) => item.multiAttrsTotalRuntime)
-    .concat(list.value.map((item) => item.multiAttrsTotalMileage))
-    .flatMap((item) => Object.values(item ?? {}))
-  return all.some((item) => Boolean(item))
-})
+type DeviceMainAlarmRow = IotDeviceVO & {
+  mainDistance?: string | number | null
+  workOrderId?: number
+  planId?: number
+  responsibleNames?: string
+}
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由跳转
-const modelFormRef = ref()
-const loading = ref(true) // 列表的加载中
-const ifShow = ref(false)
+interface QueryParams extends PageParam {
+  deptId?: number
+  deviceCode?: string
+  deviceName?: string
+  brand?: string
+  model?: string | number
+  deviceStatus?: string
+  assetProperty?: string
+  picUrl?: string
+  remark?: string
+  manufacturerId?: number
+  supplierId?: number
+  manDate?: string[]
+  nameplate?: string
+  expires?: number
+  plPrice?: number
+  plDate?: string[]
+  plYear?: number
+  plStartDate?: string[]
+  plMonthed?: number
+  plAmounted?: number
+  remainAmount?: number
+  infoId?: number
+  infoType?: string
+  infoName?: string
+  infoRemark?: string
+  infoUrl?: string
+  templateJson?: string
+  creator?: string
+  setFlag?: string
+}
+
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<DeviceMainAlarmRow>()
+
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
+  deptId: undefined,
   deviceCode: undefined,
   deviceName: undefined,
   brand: undefined,
   model: undefined,
-  deptId: undefined,
   deviceStatus: undefined,
   assetProperty: undefined,
   picUrl: undefined,
@@ -219,14 +87,34 @@ const queryParams = reactive({
   templateJson: undefined,
   creator: undefined,
   setFlag: ''
+}
+
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<DeviceMainAlarmRow[]>([])
+const total = ref(0)
+const flag = ref()
+const modelFormRef = ref()
+
+const showRunTime = computed(() => {
+  return list.value.some((item) => Boolean(item.totalRunTime))
+})
+
+const showMileage = computed(() => {
+  return list.value.some((item) => Boolean(item.totalMileage))
+})
+
+const showMultiAttrs = computed(() => {
+  const values = list.value
+    .map((item) => item.multiAttrsTotalRuntime)
+    .concat(list.value.map((item) => item.multiAttrsTotalMileage))
+    .flatMap((item) => Object.values(item ?? {}))
+
+  return values.some((item) => Boolean(item))
 })
-const queryFormRef = ref() // 搜索的表单
-const flag = ref() // 查询保养计划或保养工单的标识
-const exportLoading = ref(false) // 导出的加载中
-const contentSpan = ref(20)
-const treeShow = ref(true)
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
@@ -238,110 +126,384 @@ const getList = async () => {
   }
 }
 
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  await getList()
-}
-
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-const getDistanceClass = (distance: number | string | null) => {
-  if (distance === null || distance === undefined) return ''
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
 
-  // 如果是数字类型,直接处理
-  if (typeof distance === 'number') {
-    return distance < 0 ? 'negative-distance' : distance > 0 ? 'positive-distance' : ''
-  }
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
 
-  // 如果是字符串,提取数字部分
-  if (typeof distance === 'string') {
-    // 使用正则提取数字部分(包括负号、小数点和科学计数法)
-    const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
+const handleDeptNodeClick = async (row: Tree) => {
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
 
-    // 如果提取到数字部分,转换为数值
-    if (numericPart) {
-      const num = parseFloat(numericPart)
-      return num < 0 ? 'negative-distance' : num > 0 ? 'positive-distance' : ''
-    }
-  }
+const parseDistanceNumber = (distance: number | string | null | undefined) => {
+  if (distance === null || distance === undefined || distance === '') return undefined
+  if (typeof distance === 'number') return distance
+  const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
+  return numericPart ? Number(numericPart) : undefined
+}
 
-  return ''
+const getDistanceClass = (distance: number | string | null | undefined) => {
+  const value = parseDistanceNumber(distance)
+  if (value === undefined || value === 0) return ''
+  return value < 0 ? 'negative-distance' : 'positive-distance'
 }
 
-// 判断是否有保养计划
-const hasMaintenancePlan = (mainDistance: any) => {
-  // 检查:非null、非undefined、非空字符串
-  return mainDistance != null && mainDistance !== ''
+const hasMaintenancePlan = (mainDistance: unknown) => {
+  return mainDistance !== null && mainDistance !== undefined && mainDistance !== ''
 }
 
 const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
 }
 
-const drawerVisible = ref<boolean>(false)
-const showDrawer = ref()
-
-const openBomForm = async (row) => {
-  // 构建设备信息对象,包含所有需要的属性
+const openBomForm = async (row: DeviceMainAlarmRow) => {
   const deviceInfo = {
     deviceId: row.id,
     deviceCode: row.deviceCode,
     deviceName: row.deviceName,
-    model: row.model // 新增 model 属性
+    model: row.model
   }
+
   if (row.workOrderId) {
     flag.value = 'workOrder'
-    modelFormRef.value.open(row.workOrderId, flag.value, row.id)
+    modelFormRef.value?.open(row.workOrderId, flag.value, row.id)
   } else if (row.planId) {
     flag.value = 'plan'
-    modelFormRef.value.open(row.planId, flag.value, deviceInfo)
+    modelFormRef.value?.open(row.planId, flag.value, deviceInfo)
   }
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
+  exportLoading.value = true
   try {
-    exportLoading.value = true
     const data = await IotDeviceApi.exportIotDeviceMainAlarm(queryParams)
     download.excel(data, '保养查询.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
-const { wsCache } = useCache()
-/** 初始化 **/
+
 onMounted(() => {
   getList()
 })
 </script>
+
+<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))]"
+  >
+    <DeptTreeSelect
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="device-main-alarm-tree row-span-2"
+      @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"
+    >
+      <div class="query-row">
+        <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('iotDevice.name')" 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="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-device: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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                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 }">
+                  <el-link :underline="false" type="primary" @click="handleDetail(row.id)">
+                    {{ row.deviceName }}
+                  </el-link>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('bomList.serviceDue')" min-width="140">
+                <template #default="{ row }">
+                  <template v-if="hasMaintenancePlan(row.mainDistance)">
+                    <span :class="getDistanceClass(row.mainDistance)">
+                      {{ row.mainDistance }}
+                    </span>
+                  </template>
+                  <span v-else>无保养计划</span>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                v-if="showRunTime"
+                prop="totalRunTime"
+                label="累计运行时长H"
+                min-width="140"
+              />
+              <ZmTableColumn
+                v-if="showMileage"
+                prop="totalMileage"
+                label="累计运行里程KM"
+                min-width="150"
+              />
+              <ZmTableColumn v-if="showMultiAttrs" label="多属性累计值" min-width="200">
+                <template #default="{ row }">
+                  <template v-for="(value, key) in row.multiAttrsTotalRuntime" :key="key">
+                    <span v-if="value && key">{{ key }}: {{ value }}&nbsp;&nbsp;</span>
+                  </template>
+                  <template v-for="(value, key) in row.multiAttrsTotalMileage" :key="key">
+                    <span v-if="value && key">{{ key }}: {{ value }}&nbsp;&nbsp;</span>
+                  </template>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="deptName" :label="t('iotDevice.dept')" />
+              <ZmTableColumn prop="responsibleNames" :label="t('devicePerson.rp')" />
+              <ZmTableColumn prop="deviceStatus" :label="t('monitor.status')">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="row.deviceStatus" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('operationFill.status')" min-width="150">
+                <template #default="{ row }">
+                  <span v-if="row.shouldWorkOrder && row.runningWorkOrder">
+                    {{ t('mainPlan.generatedNotExecuted') }}
+                  </span>
+                  <span v-else-if="row.shouldWorkOrder && !row.runningWorkOrder">
+                    {{ t('mainPlan.notGenerated') }}
+                  </span>
+                  <span v-else>-</span>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('monitor.operation')" width="80" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    v-if="hasMaintenancePlan(row.mainDistance)"
+                    link
+                    type="primary"
+                    @click="openBomForm(row)"
+                    v-hasPermi="['rq:iot-device:query']"
+                  >
+                    {{ t('monitor.details') }}
+                  </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>
+
+  <DeviceAlarmBomList ref="modelFormRef" :flag="flag" />
+</template>
+
 <style scoped>
-/* 正数样式 - 淡绿色 */
+.device-main-alarm-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: 200px;
+}
+
 .positive-distance {
-  color: #67c23a; /* element-plus 成功色 */
-  background-color: rgba(103, 194, 58, 0.1); /* 10% 透明度的淡绿色背景 */
+  display: inline-block;
   padding: 2px 8px;
+  color: #67c23a;
+  background-color: rgb(103 194 58 / 10%);
   border-radius: 4px;
-  display: inline-block;
 }
 
-/* 负数样式 - 淡红色 */
 .negative-distance {
-  color: #f56c6c; /* element-plus 危险色 */
-  background-color: rgba(245, 108, 108, 0.1); /* 10% 透明度的淡红色背景 */
+  display: inline-block;
   padding: 2px 8px;
+  color: #f56c6c;
+  background-color: rgb(245 108 108 / 10%);
   border-radius: 4px;
-  display: inline-block;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2200px) {
+  .device-main-alarm-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .device-main-alarm-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 180px;
+  }
+}
+
+@media (width <= 1200px) {
+  .device-main-alarm-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(.device-main-alarm-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .device-main-alarm-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .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%;
+    margin-left: 0;
+  }
 }
 </style>

+ 520 - 680
src/views/pms/iotmainworkorder/index.vue

@@ -1,305 +1,56 @@
-<template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('bomList.name')" prop="name">
-            <el-input
-              v-model="queryParams.name"
-              :placeholder="t('bomList.nHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('bomList.status')" prop="result">
-            <el-select
-              v-model="queryParams.result"
-              :placeholder="t('bomList.status')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in resultOptions"
-                :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('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="['pms:iot-main-work-order:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['pms:iot-main-work-order:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap ref="tableContainerRef" class="table-wrap">
-        <el-table
-          v-loading="loading"
-          :data="list"
-          :stripe="true"
-          style="width: 100%"
-          ref="tableRef"
-          height="calc(85vh - 175px)"
-        >
-          <el-table-column
-            :label="t('iotDevice.serial')"
-            align="center"
-            :width="columnWidths.serial"
-            fixed="left"
-          >
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <el-table-column
-            :label="t('bomList.name')"
-            align="center"
-            prop="name"
-            :width="columnWidths.name"
-            fixed="left"
-          />
-          <el-table-column
-            :label="t('iotDevice.dept')"
-            align="center"
-            prop="deptName"
-            :width="columnWidths.deptName"
-          />
-          <el-table-column
-            :label="t('bomList.status')"
-            align="center"
-            prop="result"
-            :width="columnWidths.result"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT" :value="scope.row.result" />
-            </template>
-          </el-table-column>
-          <el-table-column
-            :label="t('bomList.serviceDue')"
-            align="center"
-            :width="columnWidths.serviceDue"
-          >
-            <template #default="scope">
-              <span :class="getDistanceClass(scope.row.mainDistance)">
-                {{ scope.row.mainDistance }}
-              </span>
-            </template>
-          </el-table-column>
-          <el-table-column
-            :label="t('bomList.type')"
-            align="center"
-            prop="type"
-            :width="columnWidths.type"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE" :value="scope.row.type" />
-            </template>
-          </el-table-column>
-          <el-table-column
-            :label="t('iotMaintain.PersonInCharge')"
-            align="center"
-            prop="responsiblePersonName"
-            :width="columnWidths.responsiblePersonName"
-          />
-          <el-table-column
-            :label="t('dict.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter2"
-            :width="columnWidths.createTime"
-          />
-          <el-table-column
-            :label="t('dict.fillTime')"
-            align="center"
-            prop="updateTime"
-            :width="columnWidths.updateTime"
-          >
-            <template #default="scope">
-              <span v-if="scope.row.result == 2">
-                {{ formatCellDate(scope.row.updateTime) }}
-              </span>
-              <span v-else></span>
-            </template>
-          </el-table-column>
-          <el-table-column
-            :label="t('iotMaintain.operation')"
-            align="center"
-            :width="columnWidths.operation"
-            fixed="right"
-          >
-            <template #default="scope">
-              <el-button
-                v-if="isNegativeMainDistance(scope.row.mainDistance)"
-                link
-                :type="getDelayReasonButtonType(scope.row.delayReason)"
-                @click="openDelayReasonDialog(scope.row)"
-                v-hasPermi="['pms:iot-main-work-order:update']"
-              >
-                {{ t('mainPlan.delayed') || '延时' }}
-              </el-button>
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['pms:iot-main-work-order:update']"
-                v-if="scope.row.result === 1"
-              >
-                {{ t('operationFill.fill') }}
-              </el-button>
-              <el-button
-                link
-                type="primary"
-                @click="detail(scope.row.id)"
-                v-hasPermi="['pms:iot-main-work-order:query']"
-              >
-                {{ t('operationFill.view') }}
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleBack(scope.row.id)"
-                v-hasPermi="['pms:iot-main-work-order:back']"
-                v-if="scope.row.result === 2 && scope.row.status === 0"
-              >
-                {{ t('workOrderMaterial.back') }}
-              </el-button>
-              <el-button
-                link
-                type="warning"
-                class="warning-btn"
-                @click="openModifyForm('modify', scope.row.id)"
-                v-hasPermi="['pms:iot-main-work-order:update']"
-                v-if="
-                  scope.row.result === 2 &&
-                  scope.row.status === 1 &&
-                  currentUserId?.toString() === scope.row.responsiblePerson
-                "
-              >
-                {{ t('modelTemplate.update') }}
-              </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <!-- 表单弹窗:添加/修改 -->
-  <IotMainWorkOrderForm ref="formRef" @success="getList" />
-
-  <!-- 延保原因弹窗 -->
-  <el-dialog
-    v-model="delayReasonDialogVisible"
-    :title="t('workOrderMaterial.delayReason') || '延时原因'"
-    width="500px"
-    :close-on-click-modal="false"
-  >
-    <el-form
-      :model="delayReasonForm"
-      label-width="0px"
-      :rules="delayReasonRules"
-      ref="delayReasonFormRef"
-    >
-      <el-form-item label=" " prop="delayReason" class="required-item" label-width="16px">
-        <el-input
-          v-model="delayReasonForm.delayReason"
-          type="textarea"
-          :rows="4"
-          :placeholder="t('workOrderMaterial.inputDelayReason') || '请输入延时原因'"
-          maxlength="500"
-          show-word-limit
-        />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="delayReasonDialogVisible = false">
-        {{ t('common.cancel') || '取消' }}
-      </el-button>
-      <el-button type="primary" @click="saveDelayReason" :loading="saveDelayReasonLoading">
-        {{ t('common.save') || '保存' }}
-      </el-button>
-    </template>
-  </el-dialog>
-</template>
-
 <script setup lang="ts">
-import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
-import IotMainWorkOrderForm from './IotMainWorkOrderForm.vue'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { useUserStore } from '@/store/modules/user'
-const { push } = useRouter() // 路由跳转
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import download from '@/utils/download'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import type { FormInstance } from 'element-plus'
 
-/** 保养工单 列表 */
 defineOptions({ name: 'IotMainWorkOrder' })
 
-// 表单引用
-const delayReasonFormRef = ref<InstanceType<typeof ElForm>>()
-
-// 定义响应式变量存储当前登录人ID
-const currentUserId = ref<number | undefined>(undefined)
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const tableRef = ref() // 表格引用
-const isLeftContentCollapsed = ref(false)
-// 表格容器引用 用于获取容器宽度
-const tableContainerRef = ref()
-const loading = ref(true) // 列表的加载中
-const list = ref<IotMainWorkOrderVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+type MainWorkOrderRow = IotMainWorkOrderVO & {
+  mainDistance?: string | number | null
+  updateTime?: string | null
+  createTime?: string | null
+}
+
+interface QueryParams extends PageParam {
+  planId?: number
+  planSerialNumber?: string
+  deptId?: number
+  orderNumber?: string
+  name?: string
+  type?: number
+  responsiblePerson?: string
+  responsiblePersonName?: string
+  cost?: number
+  result?: string | number
+  otherCost?: number
+  laborCost?: number
+  outsourcingFlag?: number
+  actualStartTime?: string[]
+  actualEndTime?: string[]
+  remark?: string
+  status?: number
+  processInstanceId?: string
+  auditStatus?: number
+  createTime?: string[]
+}
+
+type DelayReasonButtonType = 'success' | 'warning'
+
+const message = useMessage()
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<MainWorkOrderRow>()
+
+const userStore = useUserStore()
+const rootDeptId = 156
+const deptId = userStore.getUser.deptId || rootDeptId
+const currentUserId = ref<number | undefined>(userStore.getUser.id)
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   planId: undefined,
@@ -322,12 +73,24 @@ const queryParams = reactive({
   processInstanceId: undefined,
   auditStatus: undefined,
   createTime: []
+}
+
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<MainWorkOrderRow[]>([])
+const total = ref(0)
+
+const delayReasonDialogVisible = ref(false)
+const saveDelayReasonLoading = ref(false)
+const delayReasonFormRef = ref<FormInstance>()
+const delayReasonForm = reactive({
+  id: undefined as number | undefined,
+  delayReason: ''
 })
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
-// 定义表单验证规则
-const delayReasonRules = {
+const delayReasonRules = computed(() => ({
   delayReason: [
     {
       required: true,
@@ -335,73 +98,96 @@ const delayReasonRules = {
       trigger: 'blur'
     }
   ]
+}))
+
+const resultOptions = computed(() => [
+  {
+    label: t('operationFill.all'),
+    value: '0'
+  },
+  {
+    label: t('mainPlan.delayed'),
+    value: '3'
+  },
+  ...getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT)
+])
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotMainWorkOrderApi.sortedMainWorkOrderPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
 }
 
-// 列宽度配置
-const columnWidths = ref({
-  serial: '80px',
-  name: '200px',
-  deptName: '150px',
-  result: '120px',
-  serviceDue: '150px',
-  type: '120px',
-  responsiblePersonName: '150px',
-  createTime: '180px',
-  updateTime: '180px',
-  operation: '150px'
-})
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
 
-// 计算文本宽度
-const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span')
-  span.style.visibility = 'hidden'
-  span.style.position = 'absolute'
-  span.style.whiteSpace = 'nowrap'
-  span.style.fontSize = `${fontSize}px`
-  span.style.fontFamily = 'inherit'
-  span.innerText = text
+const resetQuery = () => {
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
 
-  document.body.appendChild(span)
-  const width = span.offsetWidth
-  document.body.removeChild(span)
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
 
-  return width
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
 }
 
-/** 延保原因相关状态 */
-const delayReasonDialogVisible = ref(false)
-const saveDelayReasonLoading = ref(false)
-const delayReasonForm = reactive({
-  id: undefined as number | undefined,
-  delayReason: ''
-})
+const handleDeptNodeClick = async (row: Tree) => {
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
+const formatCellDate = (dateString?: string | null) => {
+  if (!dateString) return ''
+  return dateFormatter(null, null, dateString)
+}
 
-/** 判断mainDistance是否为负值 */
-const isNegativeMainDistance = (mainDistance: string | null): boolean => {
-  if (!mainDistance) return false
+const parseDistanceNumber = (distance: number | string | null | undefined) => {
+  if (distance === null || distance === undefined || distance === '') return undefined
+  if (typeof distance === 'number') return distance
+  const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
+  return numericPart ? Number(numericPart) : undefined
+}
 
-  // 使用正则提取数字部分(包括负号、小数点和科学计数法)
-  const numericPart = mainDistance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
+const isNegativeMainDistance = (mainDistance: number | string | null | undefined) => {
+  const value = parseDistanceNumber(mainDistance)
+  return value !== undefined && value < 0
+}
 
-  if (numericPart) {
-    const num = parseFloat(numericPart)
-    return num < 0
-  }
+const getDistanceClass = (distance: number | string | null | undefined) => {
+  const value = parseDistanceNumber(distance)
+  if (value === undefined || value === 0) return ''
+  return value < 0 ? 'negative-distance' : 'positive-distance'
+}
 
-  return false
+const getDelayReasonButtonType = (
+  delayReason: string | null | undefined
+): DelayReasonButtonType => {
+  return delayReason ? 'success' : 'warning'
 }
 
-/** 打开延保原因弹窗 */
-const openDelayReasonDialog = (row: IotMainWorkOrderVO) => {
+const openDelayReasonDialog = (row: MainWorkOrderRow) => {
   delayReasonForm.id = row.id
   delayReasonForm.delayReason = row.delayReason || ''
   delayReasonDialogVisible.value = true
 }
 
-/** 保存延保原因 */
 const saveDelayReason = async () => {
-  // 表单验证
   if (!delayReasonFormRef.value) return
+
   const valid = await delayReasonFormRef.value.validate()
   if (!valid) return
 
@@ -410,18 +196,14 @@ const saveDelayReason = async () => {
     return
   }
 
+  saveDelayReasonLoading.value = true
   try {
-    saveDelayReasonLoading.value = true
-    // 调用更新接口,只传递id和delayReason字段
     await IotMainWorkOrderApi.updateIotMainWorkOrder({
       id: delayReasonForm.id,
       delayReason: delayReasonForm.delayReason
-    })
-
+    } as IotMainWorkOrderVO)
     message.success(t('common.success') || '保存成功')
     delayReasonDialogVisible.value = false
-
-    // 刷新列表数据
     await getList()
   } catch (error) {
     console.error('保存延保原因失败:', error)
@@ -431,405 +213,463 @@ const saveDelayReason = async () => {
   }
 }
 
-/** 根据delayReason是否有值返回按钮类型 */
-const getDelayReasonButtonType = (delayReason: string | null | undefined): string => {
-  // 如果delayReason有值(不为null、undefined且不是空字符串),返回success,否则返回warning
-  return delayReason ? 'success' : 'warning'
-}
-
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  await getList()
-}
-
-// 计算列宽度
-const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80 // 最小列宽
-  const PADDING = 25 // 列内边距
-  const FLEXIBLE_COLUMN = 'name' // 可伸缩列
-
-  // 确保表格容器存在
-  if (!tableContainerRef.value?.$el) return
-
-  const container = tableContainerRef.value.$el
-  const containerWidth = container.clientWidth
-
-  // 1. 计算所有列的最小宽度
-  const minWidths: Record<string, number> = {}
-  let totalMinWidth = 0
-
-  // 计算列最小宽度的函数
-  const calculateColumnMinWidth = (key: string, label: string, getValue: Function) => {
-    const headerWidth = getTextWidth(label) * 1.2
-    let contentMaxWidth = 0
-
-    // 计算内容最大宽度
-    list.value.forEach((row, index) => {
-      const text = String(getValue ? getValue(row, index) : row[key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    const minWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-    minWidths[key] = minWidth
-    totalMinWidth += minWidth
-    return minWidth
-  }
-
-  // 计算各列最小宽度
-  calculateColumnMinWidth(
-    'serial',
-    t('iotDevice.serial'),
-    (row: any, index: number) => `${index + 1}`
-  )
-  const nameMinWidth = calculateColumnMinWidth('name', t('bomList.name'), (row: any) => row.name)
-  calculateColumnMinWidth('deptName', t('iotDevice.dept'), (row: any) => row.deptName)
-  calculateColumnMinWidth('result', t('bomList.status'), (row: any) => {
-    const dict = getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT).find(
-      (d) => d.value === row.result
-    )
-    return dict ? dict.label : ''
-  })
-  calculateColumnMinWidth(
-    'serviceDue',
-    t('bomList.serviceDue'),
-    (row: any) => row.mainDistance || ''
-  )
-  calculateColumnMinWidth('type', t('bomList.type'), (row: any) => {
-    const dict = getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE).find(
-      (d) => d.value === row.type
-    )
-    return dict ? dict.label : ''
-  })
-  calculateColumnMinWidth(
-    'responsiblePersonName',
-    t('iotMaintain.PersonInCharge'),
-    (row: any) => row.responsiblePersonName
-  )
-  calculateColumnMinWidth('createTime', t('dict.createTime'), (row: any) =>
-    dateFormatter(null, null, row.createTime)
-  )
-  calculateColumnMinWidth('updateTime', t('dict.fillTime'), (row: any) =>
-    row.result == 2 ? formatCellDate(row.updateTime) : ''
-  )
-
-  // 操作列固定宽度
-  minWidths.operation = 160
-  totalMinWidth += 160
-
-  // 2. 计算可伸缩列最终宽度
-  const newWidths: Record<string, string> = {}
-  const availableWidth = containerWidth - 17 // 减去滚动条宽度
-
-  // 应用最小宽度到所有列
-  Object.keys(minWidths).forEach((key) => {
-    newWidths[key] = `${minWidths[key]}px`
-  })
-
-  // 计算可伸缩列需要的宽度
-  if (totalMinWidth < availableWidth) {
-    // 有剩余空间:分配给可伸缩列
-    newWidths[FLEXIBLE_COLUMN] =
-      `${minWidths[FLEXIBLE_COLUMN] + (availableWidth - totalMinWidth)}px`
-  } else {
-    // 空间不足:确保可伸缩列至少显示内容
-    newWidths[FLEXIBLE_COLUMN] = `${nameMinWidth}px`
-  }
-
-  // 3. 更新列宽配置
-  columnWidths.value = newWidths
-
-  // 4. 触发表格重新布局
-  nextTick(() => {
-    tableRef.value?.doLayout()
-  })
-}
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await IotMainWorkOrderApi.sortedMainWorkOrderPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-
-    // 数据加载后计算列宽
-    nextTick(() => {
-      calculateColumnWidths()
-      window.dispatchEvent(new Event('resize'))
-    })
-  } finally {
-    loading.value = false
-  }
-}
-
-// 日期格式化辅助函数
-const formatCellDate = (dateString: string | null) => {
-  if (!dateString) return ''
-  return dateFormatter(null, null, dateString)
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 处理退回操作 */
 const handleBack = async (id: number) => {
   try {
-    // 弹出确认提示框
-    await message.confirm('退回到提交人修改?', t('common.confirmTitle') || '确认', {
-      confirmButtonText: t('common.ok') || '确定',
-      cancelButtonText: t('common.cancel') || '取消',
-      type: 'warning'
-    })
-
-    // 调用接口提交退回请求
+    await message.confirm(
+      '退回到提交人修改?',
+      t('common.confirmTitle') || '确认',
+      t('common.ok') || '确定',
+      t('common.cancel') || '取消'
+    )
+
     await IotMainWorkOrderApi.updateIotMainWorkOrder({
-      id: id,
-      backFlag: '1' // 退回标识参数
-    })
+      id,
+      backFlag: '1'
+    } as unknown as IotMainWorkOrderVO)
 
     message.success(t('common.success') || '退回成功')
-    // 刷新列表数据
     await getList()
   } catch (error) {
-    // 如果是用户取消操作,不显示任何提示信息
-    if (error?.toString().includes('cancel') || error?.toString().includes('取消')) {
-      return // 静默返回,不显示任何提示
-    }
+    if (error?.toString().includes('cancel') || error?.toString().includes('取消')) return
 
     console.error('退回操作失败:', error)
     message.error(t('sys.api.operationFailed') || '退回失败')
   }
 }
 
-// 保养状态 下拉列表 添加数据字典外的选项
-const resultOptions = computed(() => [
-  {
-    label: t('operationFill.all'),
-    value: '0' // 空值会触发 clearable 效果
-  },
-  {
-    label: t('mainPlan.delayed'),
-    value: '3' // 空值会触发 clearable 效果
-  },
-  ...getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT)
-])
-
-const getDistanceClass = (distance: number | string | null) => {
-  if (distance === null || distance === undefined) return ''
-
-  // 如果是数字类型,直接处理
-  if (typeof distance === 'number') {
-    return distance < 0 ? 'negative-distance' : distance > 0 ? 'positive-distance' : ''
-  }
-
-  // 如果是字符串,提取数字部分
-  if (typeof distance === 'string') {
-    // 使用正则提取数字部分(包括负号、小数点和科学计数法)
-    const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
-
-    // 如果提取到数字部分,转换为数值
-    if (numericPart) {
-      const num = parseFloat(numericPart)
-      return num < 0 ? 'negative-distance' : num > 0 ? 'positive-distance' : ''
-    }
-  }
-
-  return ''
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  // 修改
-  if (typeof id === 'number') {
+const openForm = (type: 'create' | 'update', id?: number) => {
+  if (type === 'update' && typeof id === 'number') {
     push({ name: 'IotMainWorkOrderOptimize', params: { id } })
     return
-  } else {
-    push({ name: 'IotMainWorkOrderAdd', params: {} })
   }
+
+  push({ name: 'IotMainWorkOrderAdd', params: {} })
 }
 
-const openModifyForm = (type: string, id?: number) => {
-  // 修改
-  if (typeof id === 'number') {
+const openModifyForm = (type: 'modify', id?: number) => {
+  if (type === 'modify' && typeof id === 'number') {
     push({ name: 'IotMainWorkOrderModify', params: { id } })
-    return
   }
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotMainWorkOrderApi.deleteIotMainWorkOrder(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
 const detail = (id?: number) => {
   push({ name: 'IotMainWorkOrderDetail', params: { id } })
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
+  exportLoading.value = true
   try {
-    exportLoading.value = true
     const data = await IotMainWorkOrderApi.exportIotMainWorkOrderIndex(queryParams)
     download.excel(data, '保养工单.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
-// 声明 ResizeObserver 实例
-let resizeObserver: ResizeObserver | null = null
-
-/** 初始化 **/
 onMounted(() => {
-  // 获取当前登录人的id 与 保养工单创建人id 匹配修改退回工单 的权限
-  const userId = useUserStore().getUser.id
-  currentUserId.value = userId
-
   getList()
-  window.addEventListener('resize', calculateColumnWidths)
-  // 创建 ResizeObserver 监听表格容器尺寸变化
-  if (tableContainerRef.value?.$el) {
-    resizeObserver = new ResizeObserver(() => {
-      // 使用防抖避免频繁触发
-      clearTimeout(window.resizeTimer)
-      window.resizeTimer = setTimeout(() => {
-        calculateColumnWidths()
-      }, 100)
-    })
-    resizeObserver.observe(tableContainerRef.value.$el)
-  }
 })
+</script>
 
-onUnmounted(() => {
-  window.removeEventListener('resize', calculateColumnWidths)
+<template>
+  <div
+    class="main-work-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="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="main-work-order-tree row-span-2"
+      @node-click="handleDeptNodeClick"
+    />
 
-  // 清除 ResizeObserver
-  if (resizeObserver && tableContainerRef.value?.$el) {
-    resizeObserver.unobserve(tableContainerRef.value.$el)
-    resizeObserver = null
-  }
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="main-work-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="name">
+          <el-input
+            v-model="queryParams.name"
+            :placeholder="t('bomList.nHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('bomList.status')" prop="result">
+          <el-select
+            v-model="queryParams.result"
+            :placeholder="t('bomList.status')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in resultOptions"
+              :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('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="['pms:iot-main-work-order:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['pms:iot-main-work-order:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
+    </el-form>
 
-  // 清除定时器
-  if (window.resizeTimer) {
-    clearTimeout(window.resizeTimer)
-  }
-})
+    <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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="name" :label="t('bomList.name')" fixed="left" min-width="300" />
+              <ZmTableColumn prop="deptName" :label="t('iotDevice.dept')" min-width="150" />
+              <ZmTableColumn prop="result" :label="t('bomList.status')" width="120">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT" :value="row.result" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('bomList.serviceDue')" min-width="150">
+                <template #default="{ row }">
+                  <span :class="getDistanceClass(row.mainDistance)">
+                    {{ row.mainDistance }}
+                  </span>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="type" :label="t('bomList.type')" width="120">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE" :value="row.type" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="responsiblePersonName"
+                :label="t('iotMaintain.PersonInCharge')"
+                min-width="150"
+              />
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('dict.createTime')"
+                :formatter="dateFormatter2"
+                width="180"
+              />
+              <ZmTableColumn prop="updateTime" :label="t('dict.fillTime')" width="180">
+                <template #default="{ row }">
+                  <span v-if="row.result == 2">{{ formatCellDate(row.updateTime) }}</span>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('iotMaintain.operation')" width="140" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    v-if="isNegativeMainDistance(row.mainDistance)"
+                    link
+                    :type="getDelayReasonButtonType(row.delayReason)"
+                    @click="openDelayReasonDialog(row)"
+                    v-hasPermi="['pms:iot-main-work-order:update']"
+                  >
+                    {{ t('mainPlan.delayed') || '延时' }}
+                  </el-button>
+                  <el-button
+                    v-if="row.result === 1"
+                    link
+                    type="primary"
+                    @click="openForm('update', row.id)"
+                    v-hasPermi="['pms:iot-main-work-order:update']"
+                  >
+                    {{ t('operationFill.fill') }}
+                  </el-button>
+                  <el-button
+                    link
+                    type="primary"
+                    @click="detail(row.id)"
+                    v-hasPermi="['pms:iot-main-work-order:query']"
+                  >
+                    {{ t('operationFill.view') }}
+                  </el-button>
+                  <el-button
+                    v-if="row.result === 2 && row.status === 0"
+                    link
+                    type="danger"
+                    @click="handleBack(row.id)"
+                    v-hasPermi="['pms:iot-main-work-order:back']"
+                  >
+                    {{ t('workOrderMaterial.back') }}
+                  </el-button>
+                  <el-button
+                    v-if="
+                      row.result === 2 &&
+                      row.status === 1 &&
+                      currentUserId?.toString() === row.responsiblePerson
+                    "
+                    link
+                    type="warning"
+                    class="warning-btn"
+                    @click="openModifyForm('modify', row.id)"
+                    v-hasPermi="['pms:iot-main-work-order:update']"
+                  >
+                    {{ t('modelTemplate.update') }}
+                  </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>
 
-// 监听列表数据变化重新计算列宽
-watch(
-  list,
-  () => {
-    nextTick(calculateColumnWidths)
-  },
-  { deep: true }
-)
+  <el-dialog
+    v-model="delayReasonDialogVisible"
+    :title="t('workOrderMaterial.delayReason') || '延时原因'"
+    width="500px"
+    :close-on-click-modal="false"
+  >
+    <el-form
+      ref="delayReasonFormRef"
+      :model="delayReasonForm"
+      :rules="delayReasonRules"
+      label-width="0px"
+    >
+      <el-form-item label=" " prop="delayReason" class="required-item" label-width="16px">
+        <el-input
+          v-model="delayReasonForm.delayReason"
+          type="textarea"
+          :rows="4"
+          :placeholder="t('workOrderMaterial.inputDelayReason') || '请输入延时原因'"
+          maxlength="500"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="delayReasonDialogVisible = false">
+        {{ t('common.cancel') || '取消' }}
+      </el-button>
+      <el-button type="primary" :loading="saveDelayReasonLoading" @click="saveDelayReason">
+        {{ t('common.save') || '保存' }}
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
 
-// 监听左侧菜单状态变化(展开/收起)
-watch(isLeftContentCollapsed, () => {
-  // 添加延迟以确保 DOM 更新完成
-  setTimeout(calculateColumnWidths, 50)
-})
-</script>
 <style scoped>
-.leftcontent {
-  transition: width 0.3s ease;
-  position: relative;
+.main-work-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;
+  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: 220px;
+}
+
+.query-control--small {
+  width: 160px;
 }
 
-.leftcontent.collapsed {
-  overflow: visible;
+.query-control--date {
+  width: 240px;
 }
 
-/* 正数样式 - 淡绿色 */
 .positive-distance {
-  color: #67c23a; /* element-plus 成功色 */
-  background-color: rgba(103, 194, 58, 0.1); /* 10% 透明度的淡绿色背景 */
+  display: inline-block;
   padding: 2px 8px;
+  color: #67c23a;
+  background-color: rgb(103 194 58 / 10%);
   border-radius: 4px;
-  display: inline-block;
 }
 
-/* 负数样式 - 淡红色 */
 .negative-distance {
-  color: #f56c6c; /* element-plus 危险色 */
-  background-color: rgba(245, 108, 108, 0.1); /* 10% 透明度的淡红色背景 */
+  display: inline-block;
   padding: 2px 8px;
+  color: #f56c6c;
+  background-color: rgb(245 108 108 / 10%);
   border-radius: 4px;
-  display: inline-block;
 }
 
-.table-wrap {
-  overflow-x: auto; /* 允许水平滚动 */
+:deep(.required-item .el-form-item__label::before) {
+  margin-right: 2px;
+  color: #ff4d4f;
+  content: '*';
 }
 
-/* 确保所有内容不换行 */
-:deep(.el-table) {
-  min-width: 100% !important;
-  width: auto !important;
+:deep(.required-item .el-form-item__label) {
+  padding-right: 0 !important;
 }
 
-/* 表头和单元格内容不换行 */
-:deep(.el-table__header .el-table__cell .cell),
-:deep(.el-table__body .el-table__cell .cell) {
-  white-space: nowrap !important;
-  overflow: visible !important;
-  text-overflow: clip !important;
+:deep(.el-form-item) {
+  margin-bottom: 0;
 }
 
-/* 防止表头内容换行 */
-:deep(.el-table__header-wrapper) .el-table__cell > .cell {
-  white-space: nowrap;
+@media (width >= 2200px) {
+  .main-work-order-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
 }
 
-/* 确保表格行不换行 */
-:deep(.el-table__row) {
-  white-space: nowrap;
-}
+@media (width <= 1500px) {
+  .main-work-order-query,
+  .query-row {
+    gap: 12px 18px;
+  }
 
-/* 表头特别处理 */
-:deep(.el-table__header) {
-  .cell {
-    display: inline-block;
-    white-space: nowrap;
-    width: auto !important;
+  .query-control {
+    width: 200px;
+  }
+
+  .query-control--small {
+    width: 150px;
   }
-}
 
-/* 表格使用100%宽度 */
-:deep(.el-table__inner-wrapper) {
-  width: 100% !important;
+  .query-control--date {
+    width: 220px;
+  }
 }
 
-/* 延时原因 必填星号样式 */
-:deep(.required-item .el-form-item__label:before) {
-  content: '*';
-  color: #ff4d4f;
-  margin-right: 2px;
+@media (width <= 1200px) {
+  .main-work-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)
+    );
+  }
+
+  :deep(.main-work-order-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
 }
 
-/* 调整标签区域样式 */
-:deep(.required-item .el-form-item__label) {
-  padding-right: 0 !important;
+@media (width <= 768px) {
+  .main-work-order-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>

+ 319 - 150
src/views/pms/iotmaterialrequisition/index.vue

@@ -1,130 +1,44 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="90px"
-    >
-      <el-form-item label="领用单编号" prop="requisitionNumber">
-        <el-input
-          v-model="queryParams.requisitionNumber"
-          placeholder="请输入物料领用单编号"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="领用日期" prop="requisitionTime">
-        <el-date-picker
-          v-model="queryParams.requisitionTime"
-          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 @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')"
-          v-hasPermi="['pms:iot-material-requisition:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['pms:iot-material-requisition:export']"
-        >
-          <Icon icon="ep:download" 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="requisitionNumber" />
-      <el-table-column label="领用单名称" align="center" prop="name" />
-      <el-table-column label="领用人姓名" align="center" prop="responsiblePerson" />
-      <el-table-column label="总费用" align="center" prop="cost" />
-      <el-table-column
-        label="领用日期"
-        align="center"
-        prop="requisitionTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="120px">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="detail(scope.row.id)"
-            v-hasPermi="['pms:iot-material-requisition:query']"
-          >
-            {{ t('operationFill.view')  }}
-          </el-button>
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['pms:iot-material-requisition:update']"
-          >
-            编辑
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['pms:iot-material-requisition:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <IotMaterialRequisitionForm ref="formRef" @success="getList" />
-</template>
-
 <script setup lang="ts">
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import {
+  IotMaterialRequisitionApi,
+  IotMaterialRequisitionVO
+} from '@/api/pms/iotmaterialrequisition'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import { IotMaterialRequisitionApi, IotMaterialRequisitionVO } from '@/api/pms/iotmaterialrequisition'
-import IotMaterialRequisitionForm from './IotMaterialRequisitionForm.vue'
-const { push } = useRouter() // 路由跳转
+import { erpPriceTableColumnFormatter } from '@/utils'
 
-/** 物料领用 列表 */
 defineOptions({ name: 'IotMaterialRequisition' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type MaterialRequisitionRow = IotMaterialRequisitionVO & {
+  requisitionTime?: string
+}
+
+interface QueryParams extends PageParam {
+  deptId?: number
+  requisitionNumber?: string
+  name?: string
+  type?: number
+  responsiblePerson?: string
+  cost?: number
+  result?: number
+  otherCost?: number
+  laborCost?: number
+  requisitionTime?: string[]
+  reason?: string
+  remark?: string
+  status?: number
+  processInstanceId?: string
+  auditStatus?: number
+  createTime?: string[]
+}
+
+const { t } = useI18n()
+const message = useMessage()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<MaterialRequisitionRow>()
 
-const loading = ref(true) // 列表的加载中
-const list = ref<IotMaterialRequisitionVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -142,12 +56,16 @@ const queryParams = reactive({
   status: undefined,
   processInstanceId: undefined,
   auditStatus: undefined,
-  createTime: [],
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
+  createTime: []
+}
+
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<MaterialRequisitionRow[]>([])
+const total = ref(0)
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
@@ -159,69 +77,320 @@ const getList = async () => {
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-/* const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-} */
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
 
-const openForm = (type: string, id?: number) => {
-  // 修改
-  if (typeof id === 'number') {
-    push({ name: 'IotMaterialRequisitionEdit', params: {id } })
+const openForm = (type: 'create' | 'update', id?: number) => {
+  if (type === 'update' && typeof id === 'number') {
+    push({ name: 'IotMaterialRequisitionEdit', params: { id } })
     return
-  } else {
-    // 新增
-    push({ name: 'IotMaterialRequisitionAdd', params:{} })
   }
+
+  push({ name: 'IotMaterialRequisitionAdd', params: {} })
 }
 
 const detail = (id?: number) => {
-  push({ name: 'IotMaterialReqDetail', params:{id} })
+  push({ name: 'IotMaterialReqDetail', params: { id } })
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
-    // 删除的二次确认
     await message.delConfirm()
-    // 发起删除
     await IotMaterialRequisitionApi.deleteIotMaterialRequisition(id)
     message.success(t('common.delSuccess'))
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
     await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
     const data = await IotMaterialRequisitionApi.exportIotMaterialRequisition(queryParams)
     download.excel(data, '物料领用.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
-/** 初始化 **/
+const formatNumber = (value: unknown) => erpPriceTableColumnFormatter(null, null, value, null)
+
 onMounted(() => {
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="iot-material-requisition-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
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="90px"
+      class="iot-material-requisition-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0"
+    >
+      <div class="query-row">
+        <el-form-item label="领用单编号" prop="requisitionNumber">
+          <el-input
+            v-model="queryParams.requisitionNumber"
+            placeholder="请输入物料领用单编号"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="领用日期" prop="requisitionTime">
+          <el-date-picker
+            v-model="queryParams.requisitionTime"
+            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>
+
+      <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')"
+          v-hasPermi="['pms:iot-material-requisition:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['pms:iot-material-requisition: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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn
+                prop="requisitionNumber"
+                label="领用单编号"
+                min-width="170"
+                fixed="left"
+              />
+              <ZmTableColumn prop="name" label="领用单名称" min-width="180" />
+              <ZmTableColumn prop="responsiblePerson" label="领用人姓名" min-width="130" />
+              <ZmTableColumn prop="cost" label="总费用" min-width="110">
+                <template #default="{ row }">
+                  {{ formatNumber(row.cost) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="requisitionTime" label="领用日期" min-width="180">
+                <template #default="{ row }">
+                  {{ dateFormatter(row, null, row.requisitionTime) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn label="操作" width="150" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="detail(row.id)"
+                    v-hasPermi="['pms:iot-material-requisition:query']"
+                  >
+                    {{ t('operationFill.view') }}
+                  </el-button>
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', row.id)"
+                    v-hasPermi="['pms:iot-material-requisition:update']"
+                  >
+                    编辑
+                  </el-button>
+                  <el-button
+                    link
+                    type="danger"
+                    @click="handleDelete(row.id)"
+                    v-hasPermi="['pms:iot-material-requisition: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>
+</template>
+
+<style scoped>
+.iot-material-requisition-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 22px;
+  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: 220px;
+}
+
+.query-control--date {
+  width: 240px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 1800px) {
+  .iot-material-requisition-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .iot-material-requisition-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 200px;
+  }
+
+  .query-control--date {
+    width: 220px;
+  }
+}
+
+@media (width <= 1200px) {
+  .iot-material-requisition-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) {
+  .iot-material-requisition-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .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%;
+    margin-left: 0;
+  }
+}
+</style>

Файловите разлики са ограничени, защото са твърде много
+ 348 - 536
src/views/pms/iotprojectinfo/index.vue


+ 548 - 723
src/views/pms/iotprojecttask/index.vue

@@ -1,382 +1,67 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="公司" prop="companyId">
-        <el-select
-          v-model="queryParams.companyId"
-          placeholder="请选择公司"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="item in companyDeptList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="客户名称" prop="manufactureName">
-        <el-input
-          v-model="queryParams.manufactureName"
-          placeholder="请输入客户名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="合同名称" prop="contractName">
-        <el-input
-          v-model="queryParams.contractName"
-          placeholder="请输入合同名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="合同编号" prop="contractCode">
-        <el-input
-          v-model="queryParams.contractCode"
-          placeholder="请输入合同编号"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="施工队伍" prop="deptName">
-        <el-input
-          v-model="queryParams.deptName"
-          placeholder="请输入施工队伍"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="井号" prop="wellName">
-        <el-input
-          v-model="queryParams.wellName"
-          placeholder="请输入井号"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="平台井" prop="platformFlag">
-        <el-select
-          v-model="queryParams.platformFlag"
-          placeholder="请选择平台井"
-          clearable
-          class="!w-240px"
-          @change="handleQuery"
-        >
-          <el-option label="全部" value="A" />
-          <el-option label="是" value="Y" />
-          <el-option label="否" value="N" />
-        </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-220px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <!--
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['rq:iot-project-task:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button> -->
-        <!-- v-hasPermi="['rq:iot-project-task:export']" -->
-        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap ref="tableContainerRef">
-    <div class="table-container">
-      <el-table
-        ref="tableRef"
-        v-loading="loading"
-        :data="list"
-        :stripe="true"
-        style="width: 100%"
-        :cell-style="{ padding: '5px' }"
-      >
-        <el-table-column
-          :label="t('iotDevice.serial')"
-          :width="columnWidths.serial"
-          align="center"
-          v-if="false"
-        >
-          <template #default="scope">
-            {{ scope.$index + 1 }}
-          </template>
-        </el-table-column>
-        <el-table-column
-          label="客户名称"
-          align="center"
-          prop="manufactureName"
-          :width="columnWidths.manufactureName"
-          show-overflow-tooltip
-        />
-        <el-table-column
-          label="合同名称"
-          align="center"
-          prop="contractName"
-          :width="columnWidths.contractName"
-          show-overflow-tooltip
-        />
-        <el-table-column
-          label="合同编号"
-          align="center"
-          prop="contractCode"
-          :width="columnWidths.contractCode"
-        />
-        <el-table-column
-          label="井号"
-          align="center"
-          prop="wellName"
-          :width="columnWidths.wellName"
-        />
-        <!-- <el-table-column label="井型/井别" align="center" prop="wellType" />
-        <el-table-column :label="t('project.wellType')" align="center" prop="wellType" :width="columnWidths.wellType">
-          <template #default="scope">
-            <dict-tag :type="DICT_TYPE.PMS_PROJECT_WELL_TYPE" :value="scope.row.wellType" />
-          </template>
-        </el-table-column> -->
-        <el-table-column
-          label="施工地点"
-          align="center"
-          prop="location"
-          :width="columnWidths.location"
-        />
-        <el-table-column
-          label="施工队伍"
-          align="center"
-          prop="deptNames"
-          :width="columnWidths.deptNames"
-        />
-        <el-table-column
-          label="施工状态"
-          align="center"
-          prop="statusLabel"
-          :width="columnWidths.deptNames"
-        />
-        <!-- <el-table-column :label="t('project.technology')" align="center" prop="technique" :width="columnWidths.technique">
-          <template #default="scope">
-            <dict-tag :type="DICT_TYPE.PMS_PROJECT_TECHNOLOGY" :value="scope.row.technique" />
-          </template>
-        </el-table-column>
-        <el-table-column label="设计工作量" align="center" prop="workloadDesign" :width="columnWidths.workloadDesign"/> -->
-        <el-table-column
-          label="创建时间"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-          :width="columnWidths.createTime"
-        />
-        <!--
-        <el-table-column label="备注" align="center" prop="remark" /> -->
-        <el-table-column label="操作" align="center" :width="columnWidths.operation" fixed="right">
-          <template #default="scope">
-            <el-button
-              link
-              type="primary"
-              @click="openPlanDialog(scope.row)"
-              v-hasPermi="['rq:iot-project-task:update']"
-            >
-              计划
-            </el-button>
-            <el-button
-              v-if="Number(scope.row.deptId) === REPORT_DEPT_ID"
-              link
-              type="primary"
-              @click="openGenerateReportDialog(scope.row)"
-            >
-              生成日报
-            </el-button>
-            <el-button
-              link
-              type="primary"
-              @click="openForm('update', scope.row.id, scope.row.projectId)"
-              v-hasPermi="['rq:iot-project-task:update']"
-            >
-              编辑
-            </el-button>
-            <el-button
-              link
-              type="danger"
-              @click="handleDelete(scope.row.id)"
-              v-hasPermi="['rq:iot-project-task:delete']"
-            >
-              删除
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 计划 Dialog -->
-  <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>
-
-    <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">
-        <template #default="scope">
-          <el-select
-            v-model="scope.row.status"
-            placeholder="请选择施工状态"
-            clearable
-            class="w-full"
-            @change="onStatusChange(scope.row)"
-          >
-            <el-option
-              v-for="dict in workProgressDictOptions"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </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-model="scope.row.endTime"
-            type="datetime"
-            placeholder="选择结束时间"
-            value-format="YYYY-MM-DD HH:mm"
-            format="YYYY-MM-DD HH:mm"
-            class="w-full"
-            v-if="rowShowEndTime(scope.row)"
-          />
-        </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>
-
-    <template #footer>
-      <span class="dialog-footer">
-        <el-button @click="planDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="savePlan" :loading="saveLoading"> 保存 </el-button>
-      </span>
-    </template>
-  </el-dialog>
-
-  <!-- 生成日报 Dialog -->
-  <el-dialog v-model="generateReportDialogVisible" title="生成日报" width="420px">
-    <el-form size="default" label-width="80px">
-      <el-form-item label="日报日期" required>
-        <el-date-picker
-          v-model="reportDate"
-          type="date"
-          placeholder="请选择日期"
-          value-format="YYYY-MM-DD"
-          format="YYYY-MM-DD"
-          class="w-full!"
-        />
-      </el-form-item>
-    </el-form>
-
-    <template #footer>
-      <span class="dialog-footer">
-        <el-button size="default" @click="generateReportDialogVisible = false">取消</el-button>
-        <el-button
-          size="default"
-          type="primary"
-          @click="submitGenerateReport"
-          :loading="generateReportLoading"
-        >
-          确定
-        </el-button>
-      </span>
-    </template>
-  </el-dialog>
-</template>
-
 <script setup lang="ts">
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotProjectTaskScheduleApi } from '@/api/pms/iotprojecttaskschedule'
 import { IotProjectTaskApi, IotProjectTaskVO } from '@/api/pms/iotprojecttask'
+import * as DeptApi from '@/api/system/dept'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
 import dayjs from 'dayjs'
-import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import { ref, reactive, onMounted, nextTick, watch, onUnmounted } from 'vue'
-import { useRouter } from 'vue-router'
-import * as DeptApi from '@/api/system/dept' // 引入部门API
 
-/** 项目信息任务拆分 列表 */
 defineOptions({ name: 'IotProjectTask' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type ProjectTaskRow = IotProjectTaskVO & {
+  manufactureName?: string
+  contractName?: string
+  contractCode?: string
+  deptNames?: string
+  statusLabel?: string
+  createTime?: string
+}
+
+interface QueryParams extends PageParam {
+  companyId?: number
+  projectId?: number
+  manufactureName?: string
+  contractName?: string
+  contractCode?: string
+  wellName?: string
+  wellType?: string
+  location?: string
+  technique?: string
+  workloadDesign?: string
+  createTime?: string[]
+  userName?: string
+  userId?: number
+  platformFlag?: string
+  remark?: string
+  deptName?: string
+}
+
+interface PlanRow {
+  id?: number
+  status: string
+  startTime: string
+  endTime: string
+  showEndTime: boolean
+}
+
+const message = useMessage()
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<ProjectTaskRow>()
+
+const COMPLETED_STATUS = 'wg'
+const REPORT_DEPT_ID = 163
 
-const loading = ref(true) // 列表的加载中
-const list = ref<IotProjectTaskVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   companyId: undefined,
   projectId: undefined,
+  manufactureName: '',
+  contractName: undefined,
+  contractCode: undefined,
   wellName: undefined,
   wellType: undefined,
   location: undefined,
@@ -385,217 +70,110 @@ const queryParams = reactive({
   createTime: [],
   userName: undefined,
   userId: undefined,
-  manufactureName: '',
   platformFlag: '',
   remark: undefined,
   deptName: undefined
-})
-
-const dictQueryParams = reactive({
-  pageNo: 1,
-  pageSize: 50,
-  label: '',
-  status: undefined,
-  dictType: 'constructionStatus'
-})
-
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-const { push } = useRouter() // 路由跳转
-
-const COMPLETED_STATUS = 'wg'
-const REPORT_DEPT_ID = 163
-
-// 表格引用
-const tableRef = ref()
-// 表格容器引用
-const tableContainerRef = ref()
-
-// 列宽度配置
-const columnWidths = ref({
-  serial: '50px',
-  manufactureName: '200px',
-  contractName: '200px',
-  contractCode: '120px',
-  wellName: '100px',
-  wellType: '100px',
-  location: '120px',
-  deptNames: '120px',
-  technique: '100px',
-  workloadDesign: '100px',
-  createTime: '150px',
-  operation: '260px'
-})
-
-// 计算文本宽度
-const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span')
-  span.style.visibility = 'hidden'
-  span.style.position = 'absolute'
-  span.style.whiteSpace = 'nowrap'
-  span.style.fontSize = `${fontSize}px`
-  span.style.fontFamily = 'inherit'
-  span.innerText = text
-
-  document.body.appendChild(span)
-  const width = span.offsetWidth
-  document.body.removeChild(span)
-
-  return width
 }
 
-// 计划相关状态
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<ProjectTaskRow[]>([])
+const total = ref(0)
+const companyDeptList = ref<any[]>([])
+
 const planDialogVisible = ref(false)
-// const planList = ref<Array<{name: string, value: string, startTime: string, endTime: string}>>([])
-const planList = ref<
-  Array<{ id?: number; status: string; startTime: string; endTime: string; showEndTime: boolean }>
->([])
+const planList = ref<PlanRow[]>([])
 const saveLoading = ref(false)
-const currentRow = ref<IotProjectTaskVO | null>(null)
-const workProgressDictOptions = ref<any[]>([]) // 施工进度字典选项
-const companyDeptList = ref<any[]>([]) // 在公司级部门列表
-const wellTypeDictOptions = ref<any[]>([]) // 井型字典选项
-const technologyDictOptions = ref<any[]>([]) // 施工工艺字典选项
-
-// 生成日报相关状态
+const currentRow = ref<ProjectTaskRow | null>(null)
+const workProgressDictOptions = ref<any[]>([])
 const generateReportDialogVisible = ref(false)
 const generateReportLoading = ref(false)
-const generateReportRow = ref<IotProjectTaskVO | null>(null)
+const generateReportRow = ref<ProjectTaskRow | null>(null)
 const reportDate = ref('')
 
-/** 获取井型字典数据 */
-const getWellTypeDictOptions = async () => {
-  try {
-    wellTypeDictOptions.value = getIntDictOptions(DICT_TYPE.PMS_PROJECT_WELL_TYPE)
-  } catch (error) {
-    console.error('获取井型字典失败:', error)
-    wellTypeDictOptions.value = []
-  }
+const getWorkProgressDictOptions = () => {
+  workProgressDictOptions.value = getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE)
 }
 
-/** 获取施工工艺字典数据 */
-const getTechnologyDictOptions = async () => {
-  try {
-    technologyDictOptions.value = getIntDictOptions(DICT_TYPE.PMS_PROJECT_TECHNOLOGY)
-  } catch (error) {
-    console.error('获取施工工艺字典失败:', error)
-    technologyDictOptions.value = []
-  }
-}
-
-/** 时间戳转换为日期时间字符串(使用dayjs处理) */
 const timestampToDateTime = (timestamp: number | string | null | undefined): string => {
-  if (timestamp === null || timestamp === undefined || timestamp === '') {
-    return ''
-  }
-  // 转换为数字
-  let ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
+  if (timestamp === null || timestamp === undefined || timestamp === '') return ''
 
-  // 检查是否为有效数字
-  if (isNaN(ts)) {
-    console.warn('无效的时间戳:', timestamp)
-    return ''
-  }
-  // 如果时间戳是秒级,转换为毫秒级
-  if (ts < 1000000000000) {
-    ts *= 1000
-  }
-  return dayjs(ts).format('YYYY-MM-DD HH:mm')
-}
+  let value = typeof timestamp === 'string' ? Number.parseInt(timestamp, 10) : timestamp
+  if (Number.isNaN(value)) return ''
+  if (value < 1000000000000) value *= 1000
 
-/** 获取施工进度字典数据 */
-const getWorkProgressDictOptions = async () => {
-  try {
-    workProgressDictOptions.value = getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE)
-  } catch (error) {
-    console.error('获取施工进度字典失败:', error)
-    workProgressDictOptions.value = []
-  }
+  return dayjs(value).format('YYYY-MM-DD HH:mm')
 }
 
-/** 打开计划对话框 */
-const openPlanDialog = async (row: IotProjectTaskVO) => {
+const openPlanDialog = async (row: ProjectTaskRow) => {
   currentRow.value = row
-  planList.value = [] // 清空计划列表
-  try {
-    // 获取施工进度字典选项
-    await getWorkProgressDictOptions()
+  planList.value = []
 
-    // 获取已有计划数据
+  try {
+    getWorkProgressDictOptions()
     const taskSchedules = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules({
       taskId: row.id
     })
 
-    if (taskSchedules && taskSchedules.length > 0) {
-      // 如果有数据,则使用接口返回的数据初始化表格
+    if (taskSchedules?.length) {
       planList.value = taskSchedules.map((plan: any) => {
-        const statusNum = plan.status
+        const status = plan.status
         return {
           id: plan.id,
-          status: statusNum,
+          status,
           startTime: timestampToDateTime(plan.startTime),
           endTime: timestampToDateTime(plan.endTime),
-          showEndTime: statusNum !== COMPLETED_STATUS // 如果已是完工状态,则不显示结束时间列
+          showEndTime: status !== COMPLETED_STATUS
         }
       })
     }
-    // 如果没有数据,planList.value保持为空数组
 
     planDialogVisible.value = true
-  } catch (error) {
+  } catch {
     message.error('获取计划数据失败')
-    console.error('获取计划数据失败:', error)
   }
 }
 
-/** 判断某一行是否应该显示结束时间列 */
-const rowShowEndTime = (row) => {
-  return row.showEndTime
-}
+const rowShowEndTime = (row: PlanRow) => row.showEndTime
 
-/** 处理施工状态变化 */
-const onStatusChange = (row) => {
-  // 当状态变为“完工”时,隐藏结束时间列并清空结束时间;否则显示
+const onStatusChange = (row: PlanRow) => {
   if (row.status === COMPLETED_STATUS) {
     row.showEndTime = false
-    row.endTime = '' // 清空结束时间
-  } else {
-    row.showEndTime = true
+    row.endTime = ''
+    return
   }
+
+  row.showEndTime = true
 }
 
-/** 新增行 */
 const addNewRow = () => {
   planList.value.push({
-    status: '', // 默认值或空值
+    status: '',
     startTime: '',
     endTime: '',
     showEndTime: true
   })
 }
 
-/** 删除行 */
 const removeRow = (index: number) => {
   planList.value.splice(index, 1)
 }
 
-/** 保存计划 */
 const savePlan = async () => {
   try {
     saveLoading.value = true
 
-    // 验证数据
-    for (let i = 0; i < planList.value.length; i++) {
-      const item = planList.value[i]
-      if (!item.status) {
-        message.error(`第${i + 1}行请选择施工状态`)
+    for (let index = 0; index < planList.value.length; index++) {
+      if (!planList.value[index].status) {
+        message.error(`第${index + 1}行请选择施工状态`)
         return
       }
     }
 
-    // 准备提交数据
     const submitData = planList.value.map((item) => ({
-      id: item.id, // 更新时使用
+      id: item.id,
       taskId: currentRow.value?.id,
       status: item.status,
       startTime: item.startTime ? new Date(item.startTime).getTime() : null,
@@ -603,27 +181,23 @@ const savePlan = async () => {
         item.status !== COMPLETED_STATUS && item.endTime ? new Date(item.endTime).getTime() : null
     }))
 
-    // 调用保存接口
     await IotProjectTaskScheduleApi.saveTaskSchedule(submitData)
-
     message.success('保存成功')
     planDialogVisible.value = false
-  } catch (error) {
+    getList()
+  } catch {
     message.error('保存失败')
-    console.error('保存失败:', error)
   } finally {
     saveLoading.value = false
   }
 }
 
-/** 打开生成日报对话框 */
-const openGenerateReportDialog = (row: IotProjectTaskVO) => {
+const openGenerateReportDialog = (row: ProjectTaskRow) => {
   generateReportRow.value = row
   reportDate.value = ''
   generateReportDialogVisible.value = true
 }
 
-/** 生成日报 */
 const submitGenerateReport = async () => {
   if (!generateReportRow.value?.id) {
     message.error('未找到任务信息')
@@ -642,280 +216,531 @@ const submitGenerateReport = async () => {
     })
     message.success('生成日报成功')
     generateReportDialogVisible.value = false
-  } catch (error) {
+  } catch {
     message.error('生成日报失败')
-    console.error('生成日报失败:', error)
   } finally {
     generateReportLoading.value = false
   }
 }
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotProjectTaskApi.getIotProjectTaskList(queryParams)
     list.value = data.list
     total.value = data.total
-
-    // 获取数据后计算列宽
-    nextTick(() => {
-      calculateColumnWidths()
-    })
   } finally {
     loading.value = false
   }
 }
 
-// 计算列宽度
-const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80 // 最小列宽
-  const PADDING = 25 // 列内边距
-  const FLEXIBLE_COLUMNS = ['contractCode', 'wellName', 'location'] // 可伸缩列
-
-  // 确保表格容器存在
-  if (!tableContainerRef.value?.$el) return
-
-  const container = tableContainerRef.value.$el
-  const containerWidth = container.clientWidth
-
-  // 固定列的宽度
-  const FIXED_COLUMNS = {
-    manufactureName: 200,
-    contractName: 200
-  }
-
-  // 1. 计算所有列的最小宽度
-  const minWidths: Record<string, number> = {}
-  let totalMinWidth = 0
-
-  // 设置固定列的宽度
-  Object.keys(FIXED_COLUMNS).forEach((key) => {
-    minWidths[key] = FIXED_COLUMNS[key]
-    totalMinWidth += FIXED_COLUMNS[key]
-  })
-
-  // 计算列最小宽度的函数
-  const calculateColumnMinWidth = (key: string, label: string, getValue: Function) => {
-    // 如果是固定列,跳过计算
-    if (FIXED_COLUMNS[key]) return
-
-    const headerWidth = getTextWidth(label) * 1.2
-    let contentMaxWidth = 0
-
-    // 计算内容最大宽度
-    list.value.forEach((row, index) => {
-      const text = String(getValue ? getValue(row, index) : row[key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    const minWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-    minWidths[key] = minWidth
-    totalMinWidth += minWidth
-    return minWidth
-  }
-
-  // 计算各列最小宽度
-  calculateColumnMinWidth(
-    'serial',
-    t('iotDevice.serial'),
-    (row: any, index: number) => `${index + 1}`
-  )
-  // calculateColumnMinWidth('manufactureName', '客户名称', (row: any) => row.manufactureName);
-  // calculateColumnMinWidth('contractName', '合同名称', (row: any) => row.contractName);
-  calculateColumnMinWidth('contractCode', '合同编号', (row: any) => row.contractCode)
-  calculateColumnMinWidth('wellName', '井号', (row: any) => row.wellName)
-  calculateColumnMinWidth('wellType', t('project.wellType'), (row: any) => {
-    const dict = wellTypeDictOptions.value.find((d) => d.value === row.wellType)
-    return dict ? dict.label : ''
-  })
-  calculateColumnMinWidth('location', '施工地点', (row: any) => row.location)
-  calculateColumnMinWidth('deptNames', '施工队伍', (row: any) => row.deptNames)
-  calculateColumnMinWidth('technique', t('project.technology'), (row: any) => {
-    const dict = technologyDictOptions.value.find((d) => d.value === row.technique)
-    return dict ? dict.label : ''
-  })
-  calculateColumnMinWidth('workloadDesign', '设计工作量', (row: any) => row.workloadDesign)
-  calculateColumnMinWidth('createTime', '创建时间', (row: any) =>
-    dateFormatter(null, null, row.createTime)
-  )
-
-  // 操作列固定宽度
-  minWidths.operation = 260
-  totalMinWidth += 260
-
-  // 2. 计算可伸缩列最终宽度
-  const newWidths: Record<string, string> = {}
-  const availableWidth = containerWidth - 17 // 减去滚动条宽度
-
-  // 应用最小宽度到所有列
-  Object.keys(minWidths).forEach((key) => {
-    if (FIXED_COLUMNS[key]) {
-      newWidths[key] = `${FIXED_COLUMNS[key]}px`
-    } else {
-      newWidths[key] = `${minWidths[key]}px`
-    }
-  })
-
-  // 计算可伸缩列需要的宽度
-  if (totalMinWidth < availableWidth) {
-    // 有剩余空间:按比例分配给可伸缩列
-    const extraSpace = availableWidth - totalMinWidth
-    const flexibleColumnCount = FLEXIBLE_COLUMNS.length
-    const spacePerColumn = Math.floor(extraSpace / flexibleColumnCount)
-
-    FLEXIBLE_COLUMNS.forEach((key) => {
-      if (!FIXED_COLUMNS[key]) {
-        // 确保不是固定列
-        newWidths[key] = `${minWidths[key] + spacePerColumn}px`
-      }
-    })
-  }
-
-  // 3. 更新列宽配置
-  columnWidths.value = newWidths
-
-  // 4. 触发表格重新布局
-  nextTick(() => {
-    tableRef.value?.doLayout()
-  })
-}
-
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
 const openForm = (type: string, id?: number, projectId?: number) => {
   if (id === undefined) {
     push({ name: 'IotProjectTaskInfo', params: { type } })
-  } else {
-    push({ name: 'IotProjectTaskInfo', params: { type, id, projectId } })
+    return
   }
+
+  push({ name: 'IotProjectTaskInfo', params: { type, id, projectId } })
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
-    // 删除的二次确认
     await message.delConfirm()
-    // 发起删除
     await IotProjectTaskApi.deleteIotProjectTask(id)
     message.success(t('common.delSuccess'))
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 发起导出
     exportLoading.value = true
     const data = await IotProjectTaskApi.exportIotProjectTask(queryParams)
     download.excel(data, '项目信息任务拆分.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
-// 声明 ResizeObserver 实例
-let resizeObserver: ResizeObserver | null = null
-
-/** 初始化 **/
 onMounted(async () => {
-  // 获取公司级的部门 用于 公司 筛选 管理员使用
   companyDeptList.value = await DeptApi.companyLevelDepts()
-
-  getList()
-  // 预加载字典数据
   getWorkProgressDictOptions()
-  getWellTypeDictOptions()
-  getTechnologyDictOptions()
-
-  // 创建 ResizeObserver 监听表格容器尺寸变化
-  if (tableContainerRef.value?.$el) {
-    resizeObserver = new ResizeObserver(() => {
-      // 使用防抖避免频繁触发
-      clearTimeout((window as any).resizeTimer)
-      ;(window as any).resizeTimer = setTimeout(() => {
-        calculateColumnWidths()
-      }, 100)
-    })
-    resizeObserver.observe(tableContainerRef.value.$el)
-  }
+  getList()
 })
+</script>
 
-onUnmounted(() => {
-  // 清除 ResizeObserver
-  if (resizeObserver && tableContainerRef.value?.$el) {
-    resizeObserver.unobserve(tableContainerRef.value.$el)
-    resizeObserver = null
-  }
+<template>
+  <div
+    class="iot-project-task-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
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="72px"
+      class="iot-project-task-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0"
+    >
+      <div class="query-row">
+        <el-form-item label="公司" prop="companyId">
+          <el-select
+            v-model="queryParams.companyId"
+            placeholder="请选择公司"
+            clearable
+            filterable
+            class="w-full"
+          >
+            <el-option
+              v-for="item in companyDeptList"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="客户名称" prop="manufactureName">
+          <el-input
+            v-model="queryParams.manufactureName"
+            placeholder="请输入客户名称"
+            clearable
+            class="w-full"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="合同名称" prop="contractName">
+          <el-input
+            v-model="queryParams.contractName"
+            placeholder="请输入合同名称"
+            clearable
+            class="w-full"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="合同编号" prop="contractCode">
+          <el-input
+            v-model="queryParams.contractCode"
+            placeholder="请输入合同编号"
+            clearable
+            class="w-full"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="施工队伍" prop="deptName">
+          <el-input
+            v-model="queryParams.deptName"
+            placeholder="请输入施工队伍"
+            clearable
+            class="w-full"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="井号" prop="wellName">
+          <el-input
+            v-model="queryParams.wellName"
+            placeholder="请输入井号"
+            clearable
+            class="w-full"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="平台井" prop="platformFlag">
+          <el-select
+            v-model="queryParams.platformFlag"
+            placeholder="请选择平台井"
+            clearable
+            class="w-full"
+            @change="handleQuery"
+          >
+            <el-option label="全部" value="A" />
+            <el-option label="是" value="Y" />
+            <el-option label="否" value="N" />
+          </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-full"
+          />
+        </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="success" plain :loading="exportLoading" @click="handleExport">
+            <Icon icon="ep:download" class="mr-5px" />导出
+          </el-button>
+        </el-form-item>
+      </div>
+    </el-form>
 
-  // 清除定时器
-  if ((window as any).resizeTimer) {
-    clearTimeout((window as any).resizeTimer)
-  }
-})
+    <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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="manufactureName" label="客户名称" min-width="180" fixed="left" />
+              <ZmTableColumn prop="contractName" label="合同名称" min-width="220" />
+              <ZmTableColumn prop="contractCode" label="合同编号" min-width="150" />
+              <ZmTableColumn prop="wellName" label="井号" min-width="120" />
+              <ZmTableColumn prop="location" label="施工地点" min-width="140" />
+              <ZmTableColumn prop="deptNames" label="施工队伍" min-width="150" />
+              <ZmTableColumn prop="statusLabel" label="施工状态" min-width="120" />
+              <ZmTableColumn prop="createTime" label="创建时间" min-width="180">
+                <template #default="{ row }">
+                  {{ dateFormatter(row, null, row.createTime) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn label="操作" width="220" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openPlanDialog(row)"
+                    v-hasPermi="['rq:iot-project-task:update']"
+                  >
+                    计划
+                  </el-button>
+                  <el-button
+                    v-if="Number(row.deptId) === REPORT_DEPT_ID"
+                    link
+                    type="primary"
+                    @click="openGenerateReportDialog(row)"
+                  >
+                    生成日报
+                  </el-button>
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', row.id, row.projectId)"
+                    v-hasPermi="['rq:iot-project-task:update']"
+                  >
+                    编辑
+                  </el-button>
+                  <el-button
+                    link
+                    type="danger"
+                    @click="handleDelete(row.id)"
+                    v-hasPermi="['rq:iot-project-task:delete']"
+                  >
+                    删除
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
 
-// 监听列表数据变化重新计算列宽
-watch(
-  list,
-  () => {
-    nextTick(calculateColumnWidths)
-  },
-  { deep: true }
-)
-</script>
+      <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>
+
+  <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>
+
+    <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">
+        <template #default="scope">
+          <el-select
+            v-model="scope.row.status"
+            placeholder="请选择施工状态"
+            clearable
+            class="w-full"
+            @change="onStatusChange(scope.row)"
+          >
+            <el-option
+              v-for="dict in workProgressDictOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </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 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>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="planDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="saveLoading" @click="savePlan">保存</el-button>
+      </span>
+    </template>
+  </el-dialog>
+
+  <el-dialog v-model="generateReportDialogVisible" title="生成日报" width="420px">
+    <el-form size="default" label-width="80px">
+      <el-form-item label="日报日期" required>
+        <el-date-picker
+          v-model="reportDate"
+          type="date"
+          placeholder="请选择日期"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          class="w-full!"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button size="default" @click="generateReportDialogVisible = false">取消</el-button>
+        <el-button
+          size="default"
+          type="primary"
+          :loading="generateReportLoading"
+          @click="submitGenerateReport"
+        >
+          确定
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+
+  <!-- 生成日报 Dialog -->
+  <el-dialog v-model="generateReportDialogVisible" title="生成日报" width="420px">
+    <el-form size="default" label-width="80px">
+      <el-form-item label="日报日期" required>
+        <el-date-picker
+          v-model="reportDate"
+          type="date"
+          placeholder="请选择日期"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          class="w-full!"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button size="default" @click="generateReportDialogVisible = false">取消</el-button>
+        <el-button
+          size="default"
+          type="primary"
+          @click="submitGenerateReport"
+          :loading="generateReportLoading"
+        >
+          确定
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
 
 <style scoped>
-/* 表格容器样式,确保水平滚动 */
-.table-container {
-  width: 100%;
-  overflow-x: auto;
+.iot-project-task-query {
+  display: block;
 }
 
-/* 确保表格单元格内容不换行 */
-:deep(.el-table .cell) {
-  white-space: nowrap;
+.query-row {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(270px, 1fr));
+  align-items: center;
+  gap: 14px 28px;
+  min-width: 0;
 }
 
-/* 确保表格列标题不换行 */
-:deep(.el-table th > .cell) {
-  white-space: nowrap;
+.query-actions {
+  justify-self: end;
 }
 
-/* 调整表格最小宽度,确保内容完全显示 */
-:deep(.el-table) {
-  min-width: 100%;
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 8px 10px;
 }
 
-/* 为特定列设置省略号,但保持其他列正常显示 */
-:deep(.el-table td.el-table__cell),
-:deep(.el-table th.el-table__cell) {
-  overflow: hidden !important;
+.query-actions :deep(.el-button) {
+  margin-left: 0;
 }
 
-:deep(.el-table .cell) {
-  overflow: hidden !important;
-  text-overflow: ellipsis !important;
+:deep(.el-form-item) {
+  margin-bottom: 0;
 }
 
-/* 确保操作列的内容完全显示(不应用省略号) */
-:deep(.el-table-column--operation .cell) {
-  overflow: visible !important;
-  text-overflow: clip !important;
+@media (width >= 2200px) {
+  .query-actions {
+    grid-column: 4;
+  }
+}
+
+@media (width <= 1800px) {
+  .query-row {
+    grid-template-columns: repeat(3, minmax(270px, 1fr));
+  }
+
+  .query-actions {
+    justify-self: start;
+  }
+}
+
+@media (width <= 1500px) {
+  .query-row {
+    grid-template-columns: repeat(2, minmax(260px, 1fr));
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 210px;
+  }
+
+  .query-control--small {
+    width: 160px;
+  }
+
+  .query-control--date {
+    width: 280px;
+  }
+}
+
+@media (width <= 1200px) {
+  .iot-project-task-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%;
+    justify-self: start;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    justify-content: flex-start;
+  }
+}
+
+@media (width <= 768px) {
+  .iot-project-task-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-row {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .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>

+ 1 - 0
src/views/pms/iotrddailyreport/components/DailyStatistics.vue

@@ -526,6 +526,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
                         label="设备利用率"
                         prop="utilizationRate"
                         cover-formatter
+                        action
                         :real-value="
                           (row: List) => (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
                         "

+ 1 - 0
src/views/pms/iotrddailyreport/components/NonProductionEfficiency.vue

@@ -284,6 +284,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
               label="自然时间"
               min-width="92"
               cover-formatter
+              action
               :real-value="(row: ListItem) => formatNumber(row.calendarTime)"
             />
           </zm-table>

+ 8 - 2
src/views/pms/iotrddailyreport/fillDailyReport.vue

@@ -192,7 +192,13 @@ function realValue(type: any, value: string) {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="163" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="163"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
 
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
@@ -268,7 +274,7 @@ function realValue(type: any, value: string) {
               :height="height"
               show-border
             >
-              <zm-table-column label="操作" width="140" fixed="left">
+              <zm-table-column label="操作" width="140" fixed="left" action>
                 <template #default="scope">
                   <el-button
                     link

+ 8 - 2
src/views/pms/iotrddailyreport/index.vue

@@ -239,7 +239,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="163" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="163"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
 
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
@@ -498,7 +504,7 @@ onMounted(() => {
                   </el-tag>
                 </template>
               </zm-table-column>
-              <zm-table-column label="操作" width="120" fixed="right">
+              <zm-table-column label="操作" width="120" fixed="right" action>
                 <template #default="scope">
                   <el-button
                     link

+ 8 - 2
src/views/pms/iotrddailyreport/statistics.vue

@@ -169,7 +169,13 @@ function handleWellNameClick(taskId: number) {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="163" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="163"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
 
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
@@ -285,7 +291,7 @@ function handleWellNameClick(taskId: number) {
                 cover-formatter
                 :real-value="(row) => (Number(row.nonProductionRate ?? 0) * 100).toFixed(2) + '%'"
               />
-              <zm-table-column prop="manufactureName" label="甲方" />
+              <zm-table-column prop="manufactureName" label="甲方" action />
             </zm-table>
           </template>
         </el-auto-resizer>

+ 1 - 0
src/views/pms/iotrddailyreport/summary.vue

@@ -60,6 +60,7 @@ const resetQuery = () => {
       :top-id="163"
       v-model="query.deptId"
       @node-click="handleDeptNodeClick"
+      class="row-span-3"
     />
     <el-form
       size="default"

+ 7 - 1
src/views/pms/iotrhdailyreport/approval.vue

@@ -108,7 +108,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="157" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="157"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
 
     </div> -->

+ 7 - 1
src/views/pms/iotrhdailyreport/fill.vue

@@ -108,7 +108,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="157" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="157"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
       size="default"

+ 7 - 1
src/views/pms/iotrhdailyreport/index.vue

@@ -253,7 +253,13 @@ const openUnfilledDialog = () => {
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-4">
       <DeptTreeSelect :top-id="157" :deptId="deptId" v-model="query.deptId" :show-title="false" />
     </div> -->
-    <DeptTreeSelect :top-id="157" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="157"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-4"
+    />
     <el-form
       size="default"
       class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 gap-8 flex items-center justify-between"

+ 2 - 1
src/views/pms/iotrhdailyreport/rh-table.vue

@@ -264,10 +264,11 @@ function handleCurrentChange(val: number) {
               prop="capacity"
               label="产能(万方)"
               cover-formatter
+              :action="!showAction"
               :real-value="unitformatter"
             />
 
-            <zm-table-column label="操作" :width="120" fixed="right" v-if="showAction">
+            <zm-table-column label="操作" :width="120" fixed="right" action v-if="showAction">
               <template #default="scope">
                 <slot name="action" :row="scope.row"></slot>
               </template>

+ 4 - 1
src/views/pms/iotrhdailyreport/summary.vue

@@ -222,7 +222,8 @@ const columns = (type: string) => {
       : []),
     {
       label: '设备利用率(%)',
-      prop: 'utilizationRate'
+      prop: 'utilizationRate',
+      action: true
     }
   ]
 }
@@ -771,6 +772,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
       :top-id="157"
       v-model="query.deptId"
       @node-click="handleDeptNodeClick"
+      class="row-span-3"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3">
 
@@ -911,6 +913,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
                           :label="item.label"
                           :prop="item.prop"
                           :formatter="formatter"
+                          :action="item.action"
                         />
                         <zm-table-column
                           v-else-if="item.prop === 'name'"

+ 7 - 1
src/views/pms/iotrydailyreport/approval.vue

@@ -110,7 +110,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="158" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
 
     </div> -->

+ 7 - 1
src/views/pms/iotrydailyreport/fill.vue

@@ -110,7 +110,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="158" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
 
     </div> -->

+ 7 - 1
src/views/pms/iotrydailyreport/index.vue

@@ -136,7 +136,13 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[48px_1fr_auto] gap-x-4 gap-y-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="158" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-3"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3"> </div> -->
     <el-form
       size="default"

+ 6 - 2
src/views/pms/iotrydailyreport/ry-table.vue

@@ -404,9 +404,13 @@ function handleCurrentChange(val: number) {
               <zm-table-column prop="partyaResource" label="甲方资源" />
               <zm-table-column prop="otherNptTime" label="其它非生产时间" />
             </zm-table-column>
-            <zm-table-column prop="otherNptReason" label="其他非生产时间原因" />
+            <zm-table-column
+              prop="otherNptReason"
+              label="其他非生产时间原因"
+              :action="!showAction"
+            />
 
-            <zm-table-column label="操作" :width="120" fixed="right" v-if="showAction">
+            <zm-table-column label="操作" :width="120" fixed="right" action v-if="showAction">
               <template #default="scope">
                 <slot name="action" :row="scope.row"></slot>
               </template>

+ 2 - 2
src/views/pms/iotrydailyreport/ry-xj-table.vue

@@ -453,9 +453,9 @@ function handleCurrentChange(val: number) {
                   (Number(row.totalStaffNum) || 0) - (Number(row.offDutyStaffNum) || 0)
               "
             />
-            <zm-table-column prop="leaveStaffNum" label="休假人员数量" />
+            <zm-table-column prop="leaveStaffNum" label="休假人员数量" :action="!showAction" />
 
-            <zm-table-column label="操作" :width="120" fixed="right" v-if="showAction">
+            <zm-table-column label="操作" :width="120" fixed="right" action v-if="showAction">
               <template #default="scope">
                 <slot name="action" :row="scope.row"></slot>
               </template>

+ 4 - 16
src/views/pms/iotrydailyreport/summary.vue

@@ -176,7 +176,8 @@ const columns = (type: string) => {
     },
     {
       label: '设备利用率(%)',
-      prop: 'utilizationRate'
+      prop: 'utilizationRate',
+      action: true
     }
   ]
 }
@@ -587,6 +588,7 @@ const tolist = (id: number, non: boolean = false) => {
       :top-id="158"
       v-model="query.deptId"
       @node-click="handleDeptNodeClick"
+      class="row-span-3"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3"> </div> -->
     <el-form
@@ -706,6 +708,7 @@ const tolist = (id: number, non: boolean = false) => {
                           :label="item.label"
                           :prop="item.prop"
                           :formatter="formatter"
+                          :action="item.action"
                         />
                         <zm-table-column
                           v-else-if="item.prop === 'name'"
@@ -760,19 +763,4 @@ const tolist = (id: number, non: boolean = false) => {
 :deep(.el-form-item) {
   margin-bottom: 0;
 }
-
-:deep(.el-table) {
-  border-top-right-radius: 8px;
-  border-top-left-radius: 8px;
-
-  .el-table__cell {
-    height: 52px;
-  }
-
-  .el-table__header-wrapper {
-    .el-table__cell {
-      background: var(--el-fill-color-light);
-    }
-  }
-}
 </style>

+ 7 - 1
src/views/pms/iotrydailyreport/xapproval.vue

@@ -110,7 +110,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="158" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
       size="default"

+ 7 - 1
src/views/pms/iotrydailyreport/xfill.vue

@@ -110,7 +110,13 @@ onMounted(() => {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="158" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2"
+    />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <el-form
       size="default"

+ 7 - 1
src/views/pms/iotrydailyreport/xjindex.vue

@@ -135,7 +135,13 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
   <div
     class="grid grid-cols-[auto_1fr] grid-rows-[48px_1fr_auto] gap-x-4 gap-y-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
-    <DeptTreeSelect :top-id="158" :deptId="deptId" v-model="query.deptId" :show-title="false" />
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-3"
+    />
 
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3"> </div> -->
     <el-form

+ 4 - 16
src/views/pms/iotrydailyreport/xsummary.vue

@@ -182,7 +182,8 @@ const columns = (type: string) => {
     },
     {
       label: '设备利用率(%)',
-      prop: 'utilizationRate'
+      prop: 'utilizationRate',
+      action: true
     }
   ]
 }
@@ -586,6 +587,7 @@ const tolist = (id: number, non: boolean = false) => {
       :top-id="158"
       v-model="query.deptId"
       @node-click="handleDeptNodeClick"
+      class="row-span-3"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3"> </div> -->
     <el-form
@@ -705,6 +707,7 @@ const tolist = (id: number, non: boolean = false) => {
                           :label="item.label"
                           :prop="item.prop"
                           :formatter="formatter"
+                          :action="item.action"
                         />
                         <zm-table-column
                           v-else-if="item.prop === 'name'"
@@ -759,19 +762,4 @@ const tolist = (id: number, non: boolean = false) => {
 :deep(.el-form-item) {
   margin-bottom: 0;
 }
-
-:deep(.el-table) {
-  border-top-right-radius: 8px;
-  border-top-left-radius: 8px;
-
-  .el-table__cell {
-    height: 52px;
-  }
-
-  .el-table__header-wrapper {
-    .el-table__cell {
-      background: var(--el-fill-color-light);
-    }
-  }
-}
 </style>

+ 393 - 193
src/views/pms/iotsapstock/IotSapStockConfig.vue

@@ -1,153 +1,55 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId">
-        <el-select v-model="queryParams.factoryId" clearable :placeholder="t('faultForm.choose')" class="!w-240px" @change="selectedFactoryChange">
-          <el-option
-            v-for="item in factoryList"
-            :key="item.id"
-            :label="item.factoryName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item :label="t('workOrderMaterial.storageLocation')" prop="storageLocationId" style="margin-left: 48px">
-        <el-select v-model="queryParams.storageLocationId" clearable :placeholder="t('faultForm.choose')" class="!w-240px" >
-          <el-option
-            v-for="item in storageLocationList"
-            :key="item.id"
-            :label="item.storageLocationName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
-        <el-input
-          v-model="queryParams.materialCode"
-          :placeholder="t('chooseMaintain.materialCode')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
-        <el-input
-          v-model="queryParams.materialName"
-          :placeholder="t('chooseMaintain.materialName')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.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('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="['pms:iot-sap-stock:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}
-        </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['pms:iot-sap-stock:export']"
-        >
-          <Icon icon="ep:download" 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="t('workOrderMaterial.factory')" align="center" prop="factory" />
-      <el-table-column :label="t('workOrderMaterial.storageLocation')" align="center" prop="projectDepartment" />
-      <el-table-column :label="t('chooseMaintain.materialCode')" align="center" prop="materialCode" />
-      <el-table-column :label="t('chooseMaintain.materialName')" align="center" prop="materialName" />
-      <el-table-column :label="t('route.quantity')" align="center" prop="quantity" />
-      <el-table-column :label="t('workOrderMaterial.unitPrice')" align="center" prop="unitPrice" />
-      <el-table-column :label="t('workOrderMaterial.unit')" align="center" prop="unit" />
-      <el-table-column :label="t('route.safetyStock')" align="center" prop="safetyStock" :formatter="erpPriceTableColumnFormatter"/>
-      <el-table-column
-        :label="t('chooseMaintain.createTime')"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <!--
-      <el-table-column label="操作" align="center" min-width="120px">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['pms:iot-sap-stock:update']"
-            v-if="false"
-          >
-            安全库存
-          </el-button>
-        </template>
-      </el-table-column> -->
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <IotSapStockForm ref="formRef" @success="getList" />
-</template>
-
 <script setup lang="ts">
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
+import * as SapOrgApi from '@/api/system/saporg'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
-import IotSapStockForm from './IotSapStockForm.vue'
-import * as SapOrgApi from "@/api/system/saporg";
-import {erpPriceTableColumnFormatter} from "@/utils";
+import { erpPriceTableColumnFormatter } from '@/utils'
 
-/** PMS SAP 库存(通用库存/项目部库存) 列表 */
 defineOptions({ name: 'IotSapStockConfig' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由跳转
-const factoryList = ref([] as SapOrgApi.SapOrgVO[])   // 工厂列表
-const storageLocationList = ref([] as SapOrgApi.SapOrgVO[]) // 库存地点列表
+type StockConfigRow = IotSapStockVO & {
+  createTime?: string
+}
+
+interface QueryParams extends PageParam {
+  deptId?: number
+  factoryId?: number
+  factory?: string
+  storageLocationId?: number
+  projectDepartment?: string
+  materialCode?: string
+  materialName?: string
+  materialGroupName?: string
+  materialGroupId?: number
+  quantity?: number
+  unitPrice?: number
+  unit?: string
+  safetyStock?: number
+  shelvesId?: number
+  cargoLocationId?: number
+  type?: number
+  syncStatus?: number
+  syncTime?: string[]
+  syncError?: string
+  sort?: number
+  status?: number
+  remark?: string
+  configFlag?: string
+  createTime?: string[]
+}
+
+interface SelectedFactoryReqVO {
+  type: number
+  factoryCodes: string[]
+}
+
+const { t } = useI18n()
+const message = useMessage()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<StockConfigRow>()
 
-const loading = ref(true) // 列表的加载中
-const list = ref<IotSapStockVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -173,12 +75,18 @@ const queryParams = reactive({
   status: undefined,
   remark: undefined,
   configFlag: 'Y',
-  createTime: [],
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
+  createTime: []
+}
+
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<StockConfigRow[]>([])
+const total = ref(0)
+const factoryList = ref<SapOrgApi.SapOrgVO[]>([])
+const storageLocationList = ref<SapOrgApi.SapOrgVO[]>([])
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
@@ -188,82 +96,374 @@ const getList = async () => {
   } finally {
     loading.value = false
   }
-  // 加载工厂(SAP)列表
-  factoryList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(1)
-  // 加载库存地点(SAP)列表
-  storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
 }
 
-/** 搜索按钮操作 */
+const loadSapOrgOptions = async () => {
+  const [factories, storageLocations] = await Promise.all([
+    SapOrgApi.SapOrgApi.getSimpleSapOrgList(1),
+    SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+  ])
+  factoryList.value = factories
+  storageLocationList.value = storageLocations
+}
+
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  // formRef.value.open(type, id)
-  push({ name: 'IotConfigSafeStock', params:{} })
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
 }
 
-const selectedFactoryReqVO = ref({
-  type: 0, // 类型(1工厂 2成本中心 3库位)
-  factoryCodes: [] // 已经选择的SAP工厂code 列表
-})
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const openForm = () => {
+  push({ name: 'IotConfigSafeStock', params: {} })
+}
 
-/** 已经选择了 SAP工厂 */
 const selectedFactoryChange = async (selectedId: number | undefined) => {
+  queryParams.storageLocationId = undefined
 
-  // 获取选中的factoryCode数组
-  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
-  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
-
-  // 获得已经选择的 SAP 工厂 数组
-  // const factoryIds = formData.value.factoryIds
-  console.log('选择的工厂代码:', selectedFactoryCodes)
-  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
-  selectedFactoryReqVO.value.type = 3
-  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
-  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
-}
+  if (!selectedId) {
+    storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+    return
+  }
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotSapStockApi.deleteIotSapStock(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
+  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
+  const selectedFactoryReqVO: SelectedFactoryReqVO = {
+    type: 3,
+    factoryCodes: selectedFactory?.factoryCode ? [selectedFactory.factoryCode] : []
+  }
+  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO)
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
   try {
-    // 导出的二次确认
     await message.exportConfirm()
-    // 发起导出
     exportLoading.value = true
     const data = await IotSapStockApi.exportIotSapStock(queryParams)
     download.excel(data, 'PMS SAP 库存(通用库存/项目部库存).xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
-/** 初始化 **/
+const formatNumber = (value: unknown) => erpPriceTableColumnFormatter(null, null, value, null)
+
 onMounted(() => {
+  loadSapOrgOptions()
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="iot-sap-stock-config-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
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="82px"
+      class="iot-sap-stock-config-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('workOrderMaterial.factory')" prop="factoryId">
+          <el-select
+            v-model="queryParams.factoryId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+            @change="selectedFactoryChange"
+          >
+            <el-option
+              v-for="item in factoryList"
+              :key="item.id"
+              :label="item.factoryName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('workOrderMaterial.storageLocation')" prop="storageLocationId">
+          <el-select
+            v-model="queryParams.storageLocationId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+          >
+            <el-option
+              v-for="item in storageLocationList"
+              :key="item.id"
+              :label="item.storageLocationName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
+          <el-input
+            v-model="queryParams.materialCode"
+            :placeholder="t('chooseMaintain.materialCode')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
+          <el-input
+            v-model="queryParams.materialName"
+            :placeholder="t('chooseMaintain.materialName')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.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-button type="primary" plain @click="openForm" v-hasPermi="['pms:iot-sap-stock:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['pms:iot-sap-stock: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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn
+                prop="factory"
+                :label="t('workOrderMaterial.factory')"
+                min-width="120"
+              />
+              <ZmTableColumn
+                prop="projectDepartment"
+                :label="t('workOrderMaterial.storageLocation')"
+                min-width="140"
+              />
+              <ZmTableColumn
+                prop="materialCode"
+                :label="t('chooseMaintain.materialCode')"
+                min-width="150"
+              />
+              <ZmTableColumn
+                prop="materialName"
+                :label="t('chooseMaintain.materialName')"
+                min-width="220"
+                align="left"
+              />
+              <ZmTableColumn prop="quantity" :label="t('route.quantity')" min-width="100">
+                <template #default="{ row }">
+                  {{ formatNumber(row.quantity) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="unitPrice"
+                :label="t('workOrderMaterial.unitPrice')"
+                min-width="100"
+              >
+                <template #default="{ row }">
+                  {{ formatNumber(row.unitPrice) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" min-width="90" />
+              <ZmTableColumn prop="safetyStock" :label="t('route.safetyStock')" min-width="120">
+                <template #default="{ row }">
+                  {{ formatNumber(row.safetyStock) }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('chooseMaintain.createTime')"
+                min-width="180"
+                action
+              >
+                <template #default="{ row }">
+                  {{ dateFormatter(row, null, row.createTime) }}
+                </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>
+</template>
+
+<style scoped>
+.iot-sap-stock-config-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 22px;
+  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: 190px;
+}
+
+.query-control--date {
+  width: 220px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2200px) {
+  .iot-sap-stock-config-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .iot-sap-stock-config-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 176px;
+  }
+
+  .query-control--date {
+    width: 210px;
+  }
+}
+
+@media (width <= 1200px) {
+  .iot-sap-stock-config-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) {
+  .iot-sap-stock-config-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .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%;
+    margin-left: 0;
+  }
+}
+</style>

+ 487 - 504
src/views/pms/iotsapstock/index.vue

@@ -1,235 +1,56 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId">
-        <el-select
-          v-model="queryParams.factoryId"
-          clearable
-          filterable
-          :placeholder="t('faultForm.choose')"
-          class="!w-240px"
-          @change="selectedFactoryChange"
-        >
-          <el-option
-            v-for="item in factoryList"
-            :key="item.id"
-            :label="item.factoryName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        :label="t('workOrderMaterial.storageLocation')"
-        prop="storageLocationId"
-        style="margin-left: 28px"
-      >
-        <el-select
-          v-model="queryParams.storageLocationId"
-          clearable
-          filterable
-          :placeholder="t('faultForm.choose')"
-          class="!w-240px"
-        >
-          <el-option
-            v-for="item in storageLocationList"
-            :key="item.id"
-            :label="item.storageLocationName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
-        <el-input
-          v-model="queryParams.materialCode"
-          :placeholder="t('chooseMaintain.materialCode')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
-        <el-input
-          v-model="queryParams.materialName"
-          :placeholder="t('chooseMaintain.materialName')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.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 :label="t('route.IsItConfigured')" prop="configFlag" label-width="140px">
-        <el-select
-          v-model="queryParams.configFlag"
-          :placeholder="t('faultForm.choose')"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in resultOptions"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"
-          ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
-        >
-        <el-button @click="resetQuery"
-          ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
-        >
-        <!-- v-hasPermi="['pms:iot-sap-stock:export']" -->
-        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- ========== 统计信息卡片 ========== -->
-  <ContentWrap style="margin-bottom: 16px">
-    <el-card shadow="never" class="stat-card">
-      <div class="stat-container">
-        <div class="stat-item">
-          <span class="stat-label">{{ t('stock.totalQuantity') }}:</span>
-          <span class="stat-value">{{ totalQuantity.toLocaleString() }}</span>
-        </div>
-        <div class="stat-item">
-          <span class="stat-label">{{ t('stock.totalAmount') }}:</span>
-          <span class="stat-value"
-            >¥
-            {{
-              totalAmount.toLocaleString(undefined, {
-                minimumFractionDigits: 2,
-                maximumFractionDigits: 2
-              })
-            }}</span
-          >
-        </div>
-      </div>
-    </el-card>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap ref="tableContainerRef" class="table-container">
-    <el-table
-      ref="tableRef"
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="false"
-      style="width: 100%"
-    >
-      <el-table-column
-        :label="t('workOrderMaterial.factory')"
-        align="center"
-        prop="factory"
-        :width="columnWidths.factory"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.storageLocation')"
-        align="center"
-        prop="projectDepartment"
-        :width="columnWidths.projectDepartment"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.materialCode')"
-        align="center"
-        prop="materialCode"
-        :width="columnWidths.materialCode"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.materialName')"
-        align="left"
-        prop="materialName"
-        :width="columnWidths.materialName"
-      />
-      <el-table-column
-        :label="t('route.quantity')"
-        align="center"
-        prop="quantity"
-        :width="columnWidths.quantity"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.unitPrice')"
-        align="center"
-        prop="unitPrice"
-        :width="columnWidths.unitPrice"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.unit')"
-        align="center"
-        prop="unit"
-        :width="columnWidths.unit"
-      />
-      <el-table-column
-        :label="t('route.safetyStock')"
-        align="center"
-        prop="safetyStock"
-        :width="columnWidths.safetyStock"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.createTime')"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        :width="columnWidths.createTime"
-      />
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <IotSapStockForm ref="formRef" @success="getList" />
-</template>
-
 <script setup lang="ts">
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
-import IotSapStockForm from './IotSapStockForm.vue'
 import * as SapOrgApi from '@/api/system/saporg'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import CountTo from '@/components/count-to1.vue'
 
-/** PMS SAP 库存(通用库存/项目部库存) 列表 */
 defineOptions({ name: 'IotSapStock' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type StockRow = IotSapStockVO & {
+  totalQuantity?: number | string
+  totalAmount?: number | string
+  createTime?: string
+}
 
-const factoryList = ref([] as SapOrgApi.SapOrgVO[]) // 工厂列表
-const storageLocationList = ref([] as SapOrgApi.SapOrgVO[]) // 库存地点列表
-// 新增统计变量
-const totalQuantity = ref(0) // 总数量
-const totalAmount = ref(0) // 总金额
+interface QueryParams extends PageParam {
+  deptId?: number
+  factoryId?: number
+  factory?: string
+  storageLocationId?: number
+  projectDepartment?: string
+  materialCode?: string
+  materialName?: string
+  materialGroupName?: string
+  materialGroupId?: number
+  quantity?: number
+  unitPrice?: number
+  unit?: string
+  safetyStock?: number
+  shelvesId?: number
+  cargoLocationId?: number
+  type?: number
+  syncStatus?: number
+  syncTime?: string[]
+  syncError?: string
+  sort?: number
+  status?: number
+  remark?: string
+  configFlag?: string
+  createTime?: string[]
+}
 
-const loading = ref(true) // 列表的加载中
-const list = ref<IotSapStockVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+interface SelectedFactoryReqVO {
+  type: number
+  factoryCodes: string[]
+}
+
+const { t } = useI18n()
+const message = useMessage()
+const { ZmTable, ZmTableColumn } = useTableComponents<StockRow>()
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -256,363 +77,525 @@ const queryParams = reactive({
   remark: undefined,
   configFlag: 'A',
   createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-
-// 表格容器和表格的引用
-const tableContainerRef = ref()
-const tableRef = ref()
-
-// 列宽度配置
-const columnWidths = ref({
-  factory: '120px',
-  projectDepartment: '120px',
-  materialCode: '120px',
-  materialName: '200px', // 初始宽度,会被计算覆盖
-  quantity: '100px',
-  unitPrice: '100px',
-  unit: '100px',
-  safetyStock: '120px',
-  createTime: '180px'
-})
+}
 
-/** 获取滚动条宽度 */
-const getScrollbarWidth = () => {
-  const outer = document.createElement('div')
-  outer.style.visibility = 'hidden'
-  outer.style.overflow = 'scroll'
-  document.body.appendChild(outer)
-
-  const inner = document.createElement('div')
-  outer.appendChild(inner)
-
-  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
-  outer.parentNode?.removeChild(outer)
-
-  return scrollbarWidth
-}
-
-/** 计算文本宽度 */
-const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span')
-  span.style.visibility = 'hidden'
-  span.style.position = 'absolute'
-  span.style.whiteSpace = 'nowrap'
-  span.style.fontSize = `${fontSize}px`
-  span.style.fontFamily = 'inherit'
-  span.innerText = text
-
-  document.body.appendChild(span)
-  const width = span.offsetWidth
-  document.body.removeChild(span)
-
-  return width
-}
-
-/** 计算列宽度 */
-const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80 // 最小列宽
-  const PADDING = 25 // 列内边距
-  const FLEXIBLE_COLUMN = 'materialName' // 可伸缩列
-  const scrollbarWidth = getScrollbarWidth() // 动态获取滚动条宽度
-
-  if (!tableContainerRef.value?.$el || list.value.length === 0) return
-
-  const containerWidth = tableContainerRef.value.$el.clientWidth
-
-  // 需要自适应的列配置
-  const autoColumns = [
-    { key: 'factory', label: t('workOrderMaterial.factory'), getValue: (row) => row.factory },
-    {
-      key: 'projectDepartment',
-      label: t('workOrderMaterial.storageLocation'),
-      getValue: (row) => row.projectDepartment
-    },
-    {
-      key: 'materialCode',
-      label: t('chooseMaintain.materialCode'),
-      getValue: (row) => row.materialCode
-    },
-    {
-      key: 'materialName',
-      label: t('chooseMaintain.materialName'),
-      getValue: (row) => row.materialName
-    },
-    { key: 'quantity', label: t('route.quantity'), getValue: (row) => String(row.quantity) },
-    {
-      key: 'unitPrice',
-      label: t('workOrderMaterial.unitPrice'),
-      getValue: (row) => String(row.unitPrice)
-    },
-    { key: 'unit', label: t('workOrderMaterial.unit'), getValue: (row) => row.unit },
-    {
-      key: 'safetyStock',
-      label: t('route.safetyStock'),
-      getValue: (row) => String(row.safetyStock)
-    },
-    {
-      key: 'createTime',
-      label: t('chooseMaintain.createTime'),
-      getValue: (row) => dateFormatter(null, null, row.createTime)
-    }
-  ]
-
-  const newWidths: Record<string, string> = {}
-  let totalFixedWidth = 0 // 所有固定列的总宽度
-
-  // 计算除可伸缩列外的所有列宽度
-  autoColumns.forEach((col) => {
-    if (col.key === FLEXIBLE_COLUMN) return
-
-    const headerText = col.label
-    const headerWidth = getTextWidth(headerText) * 1.3 // 表头宽度(加粗效果增加30%)
-
-    // 计算内容最大宽度
-    let contentMaxWidth = 0
-    list.value.forEach((row) => {
-      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    // 取表头宽度、内容最大宽度和最小宽度的最大值
-    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-    newWidths[col.key] = `${finalWidth}px`
-    totalFixedWidth += finalWidth
-  })
-
-  // 处理可伸缩列(materialName)
-  const flexibleCol = autoColumns.find((col) => col.key === FLEXIBLE_COLUMN)
-  if (flexibleCol) {
-    const headerText = flexibleCol.label
-    const headerWidth = getTextWidth(headerText) * 1.3
-
-    let contentMaxWidth = 0
-    list.value.forEach((row) => {
-      const text = flexibleCol.getValue
-        ? String(flexibleCol.getValue(row))
-        : String(row[flexibleCol.key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-
-    // 剩余空间 = 容器宽度 - 其他列总宽度 - 垂直滚动条宽度
-    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth
-
-    // 可伸缩列的宽度取剩余空间和基础宽度的最大值
-    const flexibleWidth = Math.max(remainingWidth, baseWidth)
-    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`
-  }
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<StockRow[]>([])
+const total = ref(0)
+const totalQuantity = ref(0)
+const totalAmount = ref(0)
+const factoryList = ref<SapOrgApi.SapOrgVO[]>([])
+const storageLocationList = ref<SapOrgApi.SapOrgVO[]>([])
 
-  // 更新列宽度
-  columnWidths.value = newWidths
+const resultOptions = computed(() => [
+  { label: '全部', value: 'A' },
+  { label: '是', value: 'Y' },
+  { label: '否', value: 'N' }
+])
 
-  // 重新布局表格
-  nextTick(() => {
-    tableRef.value?.doLayout()
-  })
-}
+const stockStatCards = computed(() => [
+  {
+    label: t('stock.totalQuantity'),
+    value: totalQuantity.value,
+    prefix: '',
+    unit: '',
+    decimals: 0,
+    icon: 'i-tabler:packages text-sky'
+  },
+  {
+    label: t('stock.totalAmount'),
+    value: totalAmount.value,
+    prefix: '¥',
+    unit: '',
+    decimals: 2,
+    icon: 'i-material-symbols:payments-outline-rounded text-emerald'
+  }
+])
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotSapStockApi.getIotSapStockPage(queryParams)
     list.value = data.list
     total.value = data.total
-    // 从第一条记录中提取统计值
-    if (data.list && data.list.length > 0) {
-      // 确保取到有效的数值(第一条记录中的统计值代表整个查询结果)
-      totalQuantity.value = Number(data.list[0].totalQuantity) || 0
-      totalAmount.value = Number(data.list[0].totalAmount) || 0
-    } else {
-      // 没有数据时重置为0
-      totalQuantity.value = 0
-      totalAmount.value = 0
-    }
-    // 数据加载完成后计算列宽
-    nextTick(() => {
-      calculateColumnWidths()
-    })
+
+    const firstRow = data.list?.[0]
+    totalQuantity.value = Number(firstRow?.totalQuantity) || 0
+    totalAmount.value = Number(firstRow?.totalAmount) || 0
   } finally {
     loading.value = false
   }
-  // 加载工厂(SAP)列表
-  factoryList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(1)
-  // 加载库存地点(SAP)列表
-  storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
 }
 
-/** 搜索按钮操作 */
+const loadSapOrgOptions = async () => {
+  const [factories, storageLocations] = await Promise.all([
+    SapOrgApi.SapOrgApi.getSimpleSapOrgList(1),
+    SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+  ])
+  factoryList.value = factories
+  storageLocationList.value = storageLocations
+}
+
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
 }
 
-const selectedFactoryReqVO = ref({
-  type: 0, // 类型(1工厂 2成本中心 3库位)
-  factoryCodes: [] // 已经选择的SAP工厂code 列表
-})
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
 
-/** 已经选择了 SAP工厂 */
 const selectedFactoryChange = async (selectedId: number | undefined) => {
-  // 获取选中的factoryCode数组
-  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
-  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
-
-  // 获得已经选择的 SAP 工厂 数组
-  // const factoryIds = formData.value.factoryIds
-  console.log('选择的工厂代码:', selectedFactoryCodes)
-  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
-  selectedFactoryReqVO.value.type = 3
-  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
-  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
-}
+  queryParams.storageLocationId = undefined
 
-// 是否已经配置了安全库存 下拉列表 模拟字典项
-const resultOptions = computed(() => [
-  {
-    label: '全部',
-    value: 'A' // 空值会触发 clearable 效果
-  },
-  {
-    label: '是',
-    value: 'Y' // 空值会触发 clearable 效果
-  },
-  {
-    label: '否',
-    value: 'N' // 空值会触发 clearable 效果
+  if (!selectedId) {
+    storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+    return
   }
-])
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotSapStockApi.deleteIotSapStock(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
+  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
+  const selectedFactoryReqVO: SelectedFactoryReqVO = {
+    type: 3,
+    factoryCodes: selectedFactory?.factoryCode ? [selectedFactory.factoryCode] : []
+  }
+  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO)
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
   try {
     exportLoading.value = true
     const data = await IotSapStockApi.exportIotSapStock(queryParams)
     download.excel(data, 'SPA库存.xls')
   } catch {
+    message.error('导出失败')
   } finally {
     exportLoading.value = false
   }
 }
 
-/** 初始化 **/
 onMounted(() => {
+  loadSapOrgOptions()
   getList()
-  // 添加窗口大小变化监听
-  window.addEventListener('resize', calculateColumnWidths)
 })
+</script>
 
-onUnmounted(() => {
-  // 移除窗口大小变化监听
-  window.removeEventListener('resize', calculateColumnWidths)
-})
+<template>
+  <div
+    class="iot-sap-stock-page grid grid-rows-[auto_auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+  >
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="82px"
+      class="iot-sap-stock-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('workOrderMaterial.factory')" prop="factoryId">
+          <el-select
+            v-model="queryParams.factoryId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+            @change="selectedFactoryChange"
+          >
+            <el-option
+              v-for="item in factoryList"
+              :key="item.id"
+              :label="item.factoryName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('workOrderMaterial.storageLocation')" prop="storageLocationId">
+          <el-select
+            v-model="queryParams.storageLocationId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+          >
+            <el-option
+              v-for="item in storageLocationList"
+              :key="item.id"
+              :label="item.storageLocationName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
+          <el-input
+            v-model="queryParams.materialCode"
+            :placeholder="t('chooseMaintain.materialCode')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
+          <el-input
+            v-model="queryParams.materialName"
+            :placeholder="t('chooseMaintain.materialName')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.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>
+        <el-form-item :label="t('route.IsItConfigured')" prop="configFlag" label-width="116px">
+          <el-select
+            v-model="queryParams.configFlag"
+            :placeholder="t('faultForm.choose')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in resultOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </div>
 
-// 监听列表数据变化,重新计算列宽
-watch(
-  list,
-  () => {
-    nextTick(calculateColumnWidths)
-  },
-  { deep: true }
-)
-</script>
+      <div class="query-side">
+        <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">
+            <Icon icon="ep:download" class="mr-5px" />导出
+          </el-button>
+        </el-form-item>
+      </div>
+    </el-form>
+
+    <div class="stock-stat-grid">
+      <div v-for="item in stockStatCards" :key="item.label" class="stock-stat-card group">
+        <div class="relative z-10 flex h-full flex-col justify-center">
+          <span class="stat-label">{{ item.label }}</span>
+          <div class="flex items-baseline gap-1">
+            <span v-if="item.prefix" class="text-[12px] text-gray-400">{{ item.prefix }}</span>
+            <count-to
+              class="stat-value"
+              :start-val="0"
+              :end-val="item.value"
+              :decimals="item.decimals"
+            />
+            <span v-if="item.unit" class="stat-unit">{{ item.unit }}</span>
+          </div>
+        </div>
+        <div class="stat-icon">
+          <div :class="item.icon" class="text-5xl"></div>
+        </div>
+      </div>
+    </div>
+
+    <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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn
+                prop="factory"
+                :label="t('workOrderMaterial.factory')"
+                min-width="120"
+              />
+              <ZmTableColumn
+                prop="projectDepartment"
+                :label="t('workOrderMaterial.storageLocation')"
+                min-width="140"
+              />
+              <ZmTableColumn
+                prop="materialCode"
+                :label="t('chooseMaintain.materialCode')"
+                min-width="150"
+              />
+              <ZmTableColumn
+                prop="materialName"
+                :label="t('chooseMaintain.materialName')"
+                min-width="220"
+                align="left"
+              />
+              <ZmTableColumn prop="quantity" :label="t('route.quantity')" min-width="100" />
+              <ZmTableColumn
+                prop="unitPrice"
+                :label="t('workOrderMaterial.unitPrice')"
+                min-width="100"
+              />
+              <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" min-width="90" />
+              <ZmTableColumn prop="safetyStock" :label="t('route.safetyStock')" min-width="120" />
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('chooseMaintain.createTime')"
+                min-width="180"
+                action
+              >
+                <template #default="{ row }">
+                  {{ dateFormatter(row, null, row.createTime) }}
+                </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>
+</template>
 
 <style scoped>
-/* 统计卡片样式 */
-.stat-card {
-  border-radius: 4px;
-  border: 1px solid #ebeef5;
+.iot-sap-stock-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
 }
 
-.stat-container {
+.query-row {
   display: flex;
-  padding: 1px;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 22px;
+  min-width: 0;
 }
 
-.stat-item {
+.query-side {
   display: flex;
+  flex: 0 0 auto;
+  flex-wrap: wrap;
   align-items: center;
-  margin-right: 40px; /* 控制项间距 */
+  justify-content: flex-end;
+  gap: 12px 18px;
 }
 
 .stat-label {
-  font-weight: bold;
-  color: #606266;
-  margin-right: 8px;
+  margin-bottom: 2px;
+  font-size: 11px;
+  font-weight: 500;
+  letter-spacing: 0;
+  color: var(--el-text-color-regular);
 }
 
 .stat-value {
+  font-family: var(--el-font-family);
   font-size: 18px;
-  font-weight: bold;
-  color: #409eff;
+  font-weight: 700;
+  line-height: 1;
+  color: var(--el-text-color-primary);
+}
+
+.stat-unit {
+  font-size: 10px;
+  font-weight: 400;
+  color: var(--el-text-color-secondary);
 }
 
-/* 表格容器样式 - 确保可以水平滚动 */
-.table-container {
-  overflow-x: auto;
+.stock-stat-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
 }
 
-/* 防止表格内容换行 */
-:deep(.el-table) .cell {
-  white-space: nowrap !important;
-  overflow: visible !important;
-  text-overflow: unset !important;
+.stock-stat-card {
+  position: relative;
+  min-height: 78px;
+  padding: 10px 14px;
+  overflow: hidden;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-extra-light);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-lighter);
+  transition:
+    border-color 0.3s,
+    box-shadow 0.3s;
 }
 
-/* 确保表格行不换行 */
-:deep(.el-table__row) {
-  white-space: nowrap;
+.stock-stat-card:hover {
+  border-color: var(--el-color-primary-light-7);
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.stat-icon {
+  position: absolute;
+  right: -8px;
+  bottom: -12px;
+  opacity: 0.4;
+  transform: rotate(-10deg);
+  transition:
+    transform 0.5s,
+    opacity 0.5s;
+}
+
+.stock-stat-card:hover .stat-icon {
+  opacity: 0.55;
+  transform: scale(1.08) rotate(0);
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
 }
 
-/* 表头特别处理 */
-:deep(.el-table__header) {
-  .cell {
-    display: inline-block;
-    white-space: nowrap;
-    width: auto !important;
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+.query-control--small {
+  width: 140px;
+}
+
+.query-control--date {
+  width: 220px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2400px) {
+  .iot-sap-stock-query,
+  .query-row,
+  .query-side {
+    flex-wrap: nowrap;
   }
 }
 
-/* 表格整体布局优化 */
-:deep(.el-table__inner-wrapper) {
-  min-width: 100% !important;
-  width: auto !important;
+@media (width <= 1500px) {
+  .iot-sap-stock-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 176px;
+  }
+
+  .query-control--small {
+    width: 132px;
+  }
+
+  .query-control--date {
+    width: 210px;
+  }
 }
 
-/* 单元格内容完全显示 */
-:deep(.el-table__body-wrapper) .el-table__cell .cell {
-  display: block;
-  overflow: visible;
-  text-overflow: unset;
+@media (width <= 1200px) {
+  .iot-sap-stock-page {
+    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)
+    );
+  }
+
+  .query-side {
+    width: 100%;
+    justify-content: space-between;
+  }
+}
+
+@media (width <= 768px) {
+  .iot-sap-stock-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-side,
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--small,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .stock-stat-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .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>

+ 373 - 197
src/views/pms/maintenance/index.vue

@@ -1,162 +1,38 @@
-<template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="80px"
-        >
-          <el-form-item :label="t('main.planCode')" prop="serialNumber">
-            <el-input
-              v-model="queryParams.serialNumber"
-              :placeholder="t('main.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('operationFill.name')" prop="name">
-            <el-input
-              v-model="queryParams.name"
-              :placeholder="t('devicePerson.filterDevicePlaceholder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('operationFill.createTime')" prop="createTime">
-            <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-maintenance-plan: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-maintenance-plan:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap>
-        <el-table
-          height="calc(85vh - 175px)"
-          v-loading="loading"
-          :data="list"
-          :stripe="true"
-          :show-overflow-tooltip="true"
-        >
-          <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('main.planCode')" align="center" prop="serialNumber" />
-          <el-table-column :label="t('main.planName')" align="center" prop="name" />
-          <el-table-column
-            :label="t('iotMaintain.PersonInCharge')"
-            align="center"
-            prop="responsiblePersonName"
-          />
-          <el-table-column :label="t('maintain.status')" align="center" prop="status">
-            <template #default="scope">
-              <el-switch
-                v-model="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="handleStatusChange(scope.row)"
-              />
-            </template>
-          </el-table-column>
-          <el-table-column
-            :label="t('operationFill.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter2"
-            width="180px"
-          />
-          <el-table-column :label="t('maintain.operation')" align="center" min-width="120px">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-maintenance-plan:update']"
-              >
-                {{ t('info.edit') }}
-              </el-button>
-              <el-button
-                link
-                type="primary"
-                @click="detail(scope.row.id)"
-                v-hasPermi="['rq:iot-maintenance-plan:query']"
-              >
-                {{ t('maintain.view') }}
-              </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-</template>
-
 <script setup lang="ts">
-import { dateFormatter2 } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotMaintenancePlanApi, IotMaintenancePlanVO } from '@/api/pms/maintenance'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+import { useUserStore } from '@/store/modules/user'
 import { CommonStatusEnum } from '@/utils/constants'
-const { push } = useRouter() // 路由跳转
+import download from '@/utils/download'
+import { dateFormatter2 } from '@/utils/formatTime'
 
-/** 保养计划 列表 */
 defineOptions({ name: 'IotMaintenancePlan' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-let isLeftContentCollapsed = ref(false)
-const loading = ref(true) // 列表的加载中
-const list = ref<IotMaintenancePlanVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+type MaintenancePlanRow = Omit<IotMaintenancePlanVO, 'status'> & {
+  status?: number
+  createTime?: string
+}
+
+interface QueryParams extends PageParam {
+  deptId?: number
+  serialNumber?: string
+  name?: string
+  responsiblePerson?: string
+  responsiblePersonName?: string
+  remark?: string
+  status?: number
+  createTime?: string[]
+}
+
+const message = useMessage()
+const { t } = useI18n()
+const { push } = useRouter()
+const { ZmTable, ZmTableColumn } = useTableComponents<MaintenancePlanRow>()
+
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -167,56 +43,82 @@ const queryParams = reactive({
   remark: undefined,
   status: undefined,
   createTime: []
-})
+}
 
-// 响应式变量存储选中的部门
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<MaintenancePlanRow[]>([])
+const total = ref(0)
 const selectedDept = ref<{ id: number; name: string }>()
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  // 记录选中的部门信息
-  selectedDept.value = { id: row.id, name: row.name }
-  queryParams.deptId = row.id
-  await getList()
+
+const isValidStatus = (status: unknown): status is number =>
+  status === CommonStatusEnum.ENABLE || status === CommonStatusEnum.DISABLE
+
+const normalizeStatus = (status: unknown) => {
+  const value = typeof status === 'string' ? Number(status) : status
+  return isValidStatus(value) ? value : undefined
 }
-/** 查询列表 */
+
+const getStatusText = (status?: number) =>
+  status === CommonStatusEnum.ENABLE ? '启用' : status === CommonStatusEnum.DISABLE ? '停用' : ''
+
+const getPlanName = (row: MaintenancePlanRow) => row.name || row.serialNumber || '该'
+
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotMaintenancePlanApi.getIotMaintenancePlanPage(queryParams)
-    list.value = data.list
+    list.value = data.list.map((item: MaintenancePlanRow) => ({
+      ...item,
+      status: normalizeStatus(item.status)
+    }))
     total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
   selectedDept.value = undefined
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeptNodeClick = async (row: Tree) => {
+  selectedDept.value = { id: row.id, name: row.name }
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
 const detail = (id?: number) => {
   push({ name: 'IotMaintenancePlanDetail', params: { id } })
 }
 
-/** 添加/修改操作 */
-const openForm = (type: string, id?: number) => {
-  //修改
-  if (typeof id === 'number') {
+const openForm = (type: 'create' | 'update', id?: number) => {
+  if (type === 'update' && typeof id === 'number') {
     push({ name: 'IotMainPlanEdit', params: { id } })
     return
   }
-  // 新增 保养计划
+
   push({
     name: 'IotAddMainPlan',
     query: {
@@ -226,46 +128,320 @@ const openForm = (type: string, id?: number) => {
   })
 }
 
-const handleStatusChange = async (row: IotMaintenancePlanVO) => {
-  try {
-    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '""' + row.name + '" 保养计划吗?')
-    await IotMaintenancePlanApi.updatePlanStatus(row.id, row.status)
-    await getList()
-  } catch {
-    row.status =
-      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+const handleStatusChange = async (row: MaintenancePlanRow, value: number | string | boolean) => {
+  const status = normalizeStatus(value)
+  const currentStatus = normalizeStatus(row.status)
+  if (!row.id || status === undefined || currentStatus === undefined || status === currentStatus) {
+    return
   }
-}
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
   try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotMaintenancePlanApi.deleteIotMaintenancePlan(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
+    const text = getStatusText(status)
+    await message.confirm(`确认要${text}"${getPlanName(row)}"保养计划吗?`)
+    await IotMaintenancePlanApi.updatePlanStatus(row.id, status)
+    row.status = status
     await getList()
-  } catch {}
+  } catch {
+    row.status = currentStatus
+  }
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
+  exportLoading.value = true
   try {
-    // 发起导出
-    exportLoading.value = true
     const data = await IotMaintenancePlanApi.exportIotMaintenancePlan(queryParams)
     download.excel(data, '保养计划.xls')
-  } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
-/** 初始化 **/
 onMounted(() => {
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="maintenance-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="maintenance-tree row-span-2"
+      @node-click="handleDeptNodeClick"
+    />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="80px"
+      class="maintenance-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.planCode')" prop="serialNumber">
+          <el-input
+            v-model="queryParams.serialNumber"
+            :placeholder="t('main.codeHolder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('operationFill.name')" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            :placeholder="t('devicePerson.filterDevicePlaceholder')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('operationFill.createTime')" prop="createTime">
+          <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-maintenance-plan: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-maintenance-plan: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
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn prop="serialNumber" :label="t('main.planCode')" fixed="left" />
+              <ZmTableColumn prop="name" :label="t('main.planName')" fixed="left" />
+              <ZmTableColumn
+                prop="responsiblePersonName"
+                :label="t('iotMaintain.PersonInCharge')"
+              />
+              <ZmTableColumn prop="status" :label="t('maintain.status')" width="100">
+                <template #default="{ row }">
+                  <el-switch
+                    v-if="normalizeStatus(row.status) !== undefined"
+                    :model-value="row.status"
+                    :active-value="CommonStatusEnum.ENABLE"
+                    :inactive-value="CommonStatusEnum.DISABLE"
+                    @change="(status) => handleStatusChange(row, status)"
+                  />
+                  <span v-else>-</span>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('operationFill.createTime')"
+                :formatter="dateFormatter2"
+                width="180"
+              />
+              <ZmTableColumn :label="t('maintain.operation')" width="92" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', row.id)"
+                    v-hasPermi="['rq:iot-maintenance-plan:update']"
+                  >
+                    {{ t('info.edit') }}
+                  </el-button>
+                  <el-button
+                    link
+                    type="primary"
+                    @click="detail(row.id)"
+                    v-hasPermi="['rq:iot-maintenance-plan:query']"
+                  >
+                    {{ t('maintain.view') }}
+                  </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>
+</template>
+
+<style scoped>
+.maintenance-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: 220px;
+}
+
+.query-control--date {
+  width: 240px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2200px) {
+  .maintenance-query,
+  .query-row {
+    flex-wrap: nowrap;
+  }
+}
+
+@media (width <= 1500px) {
+  .maintenance-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 200px;
+  }
+
+  .query-control--date {
+    width: 220px;
+  }
+}
+
+@media (width <= 1200px) {
+  .maintenance-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(.maintenance-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .maintenance-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .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%;
+    margin-left: 0;
+  }
+}
+</style>

+ 23 - 35
src/views/report-statistics/costs.vue

@@ -6,6 +6,7 @@ import { IotReportApi } from '@/api/pms/report'
 import { useDebounceFn } from '@vueuse/core'
 import download from '@/utils/download'
 import { rangeShortcuts } from '@/utils/formatTime'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 
 // 定义时间类型
 type TimeType = 'year' | 'month' | 'day'
@@ -205,6 +206,7 @@ function handleReset() {
 }
 
 const exportLoading = ref(false)
+const { ZmTable, ZmTableColumn } = useTableComponents<any>()
 
 const handleExport = async () => {
   exportLoading.value = true
@@ -235,6 +237,7 @@ const handleChange = () => {
       v-model="query.deptId"
       :init-select="false"
       :show-title="false"
+      class="row-span-2"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <div class="grid grid-rows-[1fr_32px] gap-4">
@@ -355,29 +358,29 @@ const handleChange = () => {
       <div class="flex-1 relative">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">
-            <el-table
+            <ZmTable
               :data="list"
-              v-loading="loading"
-              stripe
-              class="absolute"
-              :max-height="height"
-              :height="height"
-              show-overflow-tooltip
+              :loading="loading"
               :width="width"
-              scrollbar-always-on
+              :height="height"
+              :max-height="height"
+              show-border
             >
-              <el-table-column label="序号" type="index" width="50" align="center" />
-              <el-table-column label="日期" prop="date" align="center" />
-              <el-table-column label="类别" prop="type" align="center" />
-              <el-table-column label="设备编号" prop="deviceCode" align="center" />
-              <el-table-column label="设备名称" prop="deviceName" align="center" />
-              <el-table-column
-                label="成本"
-                prop="cost"
-                align="center"
-                :formatter="(row) => (row.cost ?? 0) + '元'"
+              <ZmTableColumn
+                label="序号"
+                type="index"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
               />
-            </el-table>
+              <ZmTableColumn label="日期" prop="date" min-width="140" />
+              <ZmTableColumn label="类别" prop="type" min-width="120" />
+              <ZmTableColumn label="设备编号" prop="deviceCode" min-width="150" />
+              <ZmTableColumn label="设备名称" prop="deviceName" min-width="180" />
+              <ZmTableColumn label="成本" prop="cost" min-width="120" action>
+                <template #default="{ row }"> {{ row.cost ?? 0 }}元 </template>
+              </ZmTableColumn>
+            </ZmTable>
           </template>
         </el-auto-resizer>
       </div>
@@ -399,19 +402,4 @@ const handleChange = () => {
   </div>
 </template>
 
-<style scoped>
-:deep(.el-table) {
-  border-top-right-radius: 8px;
-  border-top-left-radius: 8px;
-
-  .el-table__cell {
-    height: 52px;
-  }
-
-  .el-table__header-wrapper {
-    .el-table__cell {
-      background: var(--el-fill-color-light);
-    }
-  }
-}
-</style>
+<style scoped></style>

+ 23 - 6
src/views/report-statistics/daily-report.vue

@@ -451,7 +451,7 @@ const handleExport = () => {
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="daily-report-page grid grid-cols-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <el-form
       size="default"
@@ -503,24 +503,28 @@ const handleExport = () => {
       </el-form-item>
     </el-form>
     <DeptTreeSelect
-      v-show="tab === '队伍'"
+      v-if="tab === '队伍'"
       :top-id="157"
       :deptId="deptId"
       v-model="query.deptId"
       title="队伍"
+      class="daily-report-side"
     />
     <WellSelect
-      v-show="tab === '井'"
+      v-if="tab === '井'"
       v-model:contract-name="query.contractName"
       :deptId="157"
       v-model:model-value="query.wellName"
+      class="daily-report-side"
     />
     <!-- 第二行左侧:自动落入第 2 行第 1 列 -->
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full"> </div> -->
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->
-    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
-      <div class="flex-1 relative">
+    <div
+      class="daily-report-table-card bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col min-h-0"
+    >
+      <div class="flex-1 relative min-h-0">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">
             <el-table
@@ -555,7 +559,7 @@ const handleExport = () => {
                   </div>
                 </template>
               </el-table-column>
-              <DailyTableColumn :columns="columns" />
+              <DailyTableColumn :columns="columns" company="rh" />
               <!-- <el-table-column label="操作" width="120px" align="center" fixed="right">
                 <template #default="{ row }">
                   <el-button link type="success" v-hasPermi="['pms:iot-rh-daily-report:query']">
@@ -594,6 +598,19 @@ const handleExport = () => {
 </template>
 
 <style scoped>
+.daily-report-page {
+  grid-template-rows: 62px minmax(0, 1fr);
+}
+
+.daily-report-side {
+  height: 100% !important;
+  min-height: 0 !important;
+}
+
+.daily-report-table-card {
+  min-height: 0;
+}
+
 :deep(.el-form-item) {
   margin-bottom: 0;
 }

+ 20 - 3
src/views/report-statistics/rd-daily-report.vue

@@ -390,7 +390,7 @@ const handleExport = () => {
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="daily-report-page grid grid-cols-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <el-form
       size="default"
@@ -449,18 +449,22 @@ const handleExport = () => {
       :deptId="deptId"
       v-model="query.deptId"
       title="队伍"
+      class="daily-report-side"
     />
     <WellSelect
       v-show="tab === '井'"
       :deptId="163"
       v-model="query.wellName"
       v-model:contract-name="query.contractName"
+      class="daily-report-side"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full"> </div> -->
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->
-    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
-      <div class="flex-1 relative">
+    <div
+      class="daily-report-table-card bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col min-h-0"
+    >
+      <div class="flex-1 relative min-h-0">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">
             <el-table
@@ -535,6 +539,19 @@ const handleExport = () => {
 </template>
 
 <style scoped>
+.daily-report-page {
+  grid-template-rows: 62px minmax(0, 1fr);
+}
+
+.daily-report-side {
+  height: 100% !important;
+  min-height: 0 !important;
+}
+
+.daily-report-table-card {
+  min-height: 0;
+}
+
 :deep(.el-form-item) {
   margin-bottom: 0;
 }

+ 20 - 3
src/views/report-statistics/ry-daily-report.vue

@@ -556,7 +556,7 @@ const handleExport = () => {
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="daily-report-page grid grid-cols-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <el-form
       size="default"
@@ -613,12 +613,14 @@ const handleExport = () => {
       :deptId="deptId"
       v-model="query.deptId"
       title="队伍"
+      class="daily-report-side"
     />
     <WellSelect
       v-show="tab === '井'"
       :deptId="158"
       v-model="query.wellName"
       v-model:contract-name="query.contractName"
+      class="daily-report-side"
     />
 
     <!-- 第二行左侧:自动落入第 2 行第 1 列 -->
@@ -627,8 +629,10 @@ const handleExport = () => {
     </div> -->
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->
-    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
-      <div class="flex-1 relative">
+    <div
+      class="daily-report-table-card bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col min-h-0"
+    >
+      <div class="flex-1 relative min-h-0">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">
             <el-table
@@ -706,6 +710,19 @@ const handleExport = () => {
 </template>
 
 <style scoped>
+.daily-report-page {
+  grid-template-rows: 62px minmax(0, 1fr);
+}
+
+.daily-report-side {
+  height: 100% !important;
+  min-height: 0 !important;
+}
+
+.daily-report-table-card {
+  min-height: 0;
+}
+
 :deep(.el-form-item) {
   margin-bottom: 0;
 }

+ 20 - 3
src/views/report-statistics/ry-xj-daily-report.vue

@@ -497,7 +497,7 @@ const handleExport = () => {
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="daily-report-page grid grid-cols-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <el-form
       size="default"
@@ -555,12 +555,14 @@ const handleExport = () => {
       :deptId="deptId"
       v-model="query.deptId"
       title="队伍"
+      class="daily-report-side"
     />
     <WellSelect
       v-show="tab === '井'"
       :deptId="158"
       v-model="query.wellName"
       v-model:contract-name="query.contractName"
+      class="daily-report-side"
     />
     <!-- 第二行左侧:自动落入第 2 行第 1 列 -->
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full">
@@ -568,8 +570,10 @@ const handleExport = () => {
     </div> -->
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->
-    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
-      <div class="flex-1 relative">
+    <div
+      class="daily-report-table-card bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col min-h-0"
+    >
+      <div class="flex-1 relative min-h-0">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">
             <el-table
@@ -647,6 +651,19 @@ const handleExport = () => {
 </template>
 
 <style scoped>
+.daily-report-page {
+  grid-template-rows: 62px minmax(0, 1fr);
+}
+
+.daily-report-side {
+  height: 100% !important;
+  min-height: 0 !important;
+}
+
+.daily-report-table-card {
+  min-height: 0;
+}
+
 :deep(.el-form-item) {
   margin-bottom: 0;
 }

+ 27 - 54
src/views/report-statistics/work-order-completion.vue

@@ -7,6 +7,7 @@ import CountTo from '@/components/count-to1.vue'
 import { IotReportApi } from '@/api/pms/report'
 import { rangeShortcuts } from '@/utils/formatTime'
 import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 
 // 定义时间类型
 type TimeType = 'year' | 'month' | 'day'
@@ -261,6 +262,7 @@ const handleChange = () => {
 const message = useMessage()
 
 const exportLoading = ref(false)
+const { ZmTable, ZmTableColumn } = useTableComponents<any>()
 
 async function handleExport() {
   try {
@@ -286,6 +288,7 @@ async function handleExport() {
       v-model="query.deptId"
       :init-select="false"
       :show-title="false"
+      class="row-span-2"
     />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <div class="flex flex-col gap-4 h-full">
@@ -417,48 +420,33 @@ async function handleExport() {
       <div class="flex-1 relative">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">
-            <el-table
+            <ZmTable
               :data="list"
-              v-loading="loading"
-              stripe
-              class="absolute"
-              :max-height="height"
-              :height="height"
-              show-overflow-tooltip
+              :loading="loading"
               :width="width"
-              scrollbar-always-on
+              :height="height"
+              :max-height="height"
+              show-border
             >
-              <el-table-column label="序号" type="index" width="60" align="center" />
-              <el-table-column label="工单类别" prop="type" align="center" width="80" />
-              <el-table-column
-                label="生成日期"
-                prop="createTime"
-                align="center"
-                width="160"
-                :formatter="(row) => row.createTime.split(' ')[0]"
+              <ZmTableColumn
+                label="序号"
+                type="index"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
               />
-              <el-table-column label="公司" prop="company" align="center" width="100" />
-              <el-table-column label="项目部" prop="project" align="center" />
-              <el-table-column label="队伍" prop="deptName" align="center" />
-              <el-table-column label="状态" prop="status" align="center" width="80">
-                <!-- <template #default="{ row }">
-                  <el-tag v-if="row.status === '已完成'" type="success" effect="dark" size="small"
-                    >已完成</el-tag
-                  >
-                  <el-tag
-                    v-else-if="row.status === '未完成'"
-                    type="danger"
-                    effect="dark"
-                    size="small"
-                    >未完成</el-tag
-                  >
-                  <el-tag v-else type="warning" effect="plain" size="small">{{
-                    row.status
-                  }}</el-tag>
-                </template> -->
-              </el-table-column>
-              <el-table-column label="设备" prop="device" align="center" />
-            </el-table>
+              <ZmTableColumn label="工单类别" prop="type" min-width="110" />
+              <ZmTableColumn label="生成日期" prop="createTime" min-width="140">
+                <template #default="{ row }">
+                  {{ row.createTime ? row.createTime.split(' ')[0] : '' }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn label="公司" prop="company" min-width="120" />
+              <ZmTableColumn label="项目部" prop="project" min-width="160" />
+              <ZmTableColumn label="队伍" prop="deptName" min-width="140" />
+              <ZmTableColumn label="状态" prop="status" min-width="100" />
+              <ZmTableColumn label="设备" prop="device" min-width="180" action />
+            </ZmTable>
           </template>
         </el-auto-resizer>
       </div>
@@ -480,19 +468,4 @@ async function handleExport() {
   </div>
 </template>
 
-<style scoped>
-:deep(.el-table) {
-  border-top-right-radius: 8px;
-  border-top-left-radius: 8px;
-
-  .el-table__cell {
-    height: 52px;
-  }
-
-  .el-table__header-wrapper {
-    .el-table__cell {
-      background: var(--el-fill-color-light);
-    }
-  }
-}
-</style>
+<style scoped></style>

Някои файлове не бяха показани, защото твърде много файлове са промени