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

Merge branch 'refactor/page'

Zimo пре 8 часа
родитељ
комит
bbdac5477f
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>

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