Răsfoiți Sursa

qhse相关调整

yanghao 1 săptămână în urmă
părinte
comite
ee1a6854c0
35 a modificat fișierele cu 10839 adăugiri și 6188 ștergeri
  1. 1 1
      .env.local
  2. 3 0
      src/api/pms/stat/index.ts
  3. 2 0
      src/components/ZmTable/index.vue
  4. 307 168
      src/views/pms/device/CustomerList.vue
  5. 409 173
      src/views/pms/failure/IotFailureReportForm.vue
  6. 398 289
      src/views/pms/failure/index.vue
  7. 189 45
      src/views/pms/information/IotInformationDbForm.vue
  8. 220 113
      src/views/pms/information/index.vue
  9. 206 69
      src/views/pms/inspect/item/IotInspectItemForm.vue
  10. 281 148
      src/views/pms/inspect/item/index.vue
  11. 348 176
      src/views/pms/inspect/order/InspectOrderDetail.vue
  12. 938 153
      src/views/pms/inspect/order/WriteOrder.vue
  13. 312 250
      src/views/pms/inspect/order/index.vue
  14. 349 217
      src/views/pms/inspect/plan/InspectRouteList.vue
  15. 388 243
      src/views/pms/inspect/plan/IotInspectPlan.vue
  16. 276 226
      src/views/pms/inspect/plan/index.vue
  17. 306 169
      src/views/pms/inspect/route/InspectItemList.vue
  18. 410 139
      src/views/pms/inspect/route/IotInspectRoute.vue
  19. 267 144
      src/views/pms/inspect/route/index.vue
  20. 308 426
      src/views/pms/iotmainworkorder/DeviceAlarmBomList.vue
  21. 17 25
      src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue
  22. 250 115
      src/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue
  23. 515 257
      src/views/pms/iotprojectinfo/IotProjectInfoForm.vue
  24. 32 51
      src/views/pms/iotprojectinfo/index.vue
  25. 776 707
      src/views/pms/iotprojecttask/IotProjectTaskForm.vue
  26. 249 68
      src/views/pms/iotprojecttask/index.vue
  27. 512 440
      src/views/pms/maintain/IotMaintainAddEdit.vue
  28. 417 222
      src/views/pms/maintain/IotMaintainDetail.vue
  29. 417 188
      src/views/pms/maintain/IotMaintainRecord.vue
  30. 430 328
      src/views/pms/maintain/index.vue
  31. 43 22
      src/views/pms/maintain/material/MaintainMaterialDrawer.vue
  32. 661 313
      src/views/pms/stat/inspect.vue
  33. 540 287
      src/views/pms/stat/maintain.vue
  34. 58 10
      src/views/pms/stat/rdkb/rd-rate.vue
  35. 4 6
      src/views/pms/stat/rhkb/rhsummary.vue

+ 1 - 1
.env.local

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 32 - 51
src/views/pms/iotprojectinfo/index.vue

@@ -64,23 +64,23 @@
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
             class="query-control query-control--date" />
         </el-form-item>
-        <el-form-item class="query-actions">
-          <el-button type="primary" @click="handleQuery">
-            <Icon icon="ep:search" class="mr-5px" />搜索
-          </el-button>
-          <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
-          <el-button
-            type="primary"
-            plain
-            @click="openForm('create', undefined)"
-            v-hasPermi="['rq:iot-project-info:create']">
-            <Icon icon="ep:plus" class="mr-5px" />新增
-          </el-button>
-          <el-button type="success" plain :loading="exportLoading" @click="handleExport">
-            <Icon icon="ep:download" class="mr-5px" />导出
-          </el-button>
-        </el-form-item>
       </div>
+      <el-form-item class="query-actions">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />搜索
+        </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create', undefined)"
+          v-hasPermi="['rq:iot-project-info:create']">
+          <Icon icon="ep:plus" class="mr-5px" />新增
+        </el-button>
+        <el-button type="success" plain :loading="exportLoading" @click="handleExport">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
     </el-form>
 
     <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
@@ -899,17 +899,9 @@ watch(
 </script>
 
 <style scoped>
-@media (width <= 1500px) {
+@media (width <= 1600px) {
   .iot-project-info-query .query-row {
-    gap: 12px 18px;
-  }
-
-  .iot-project-info-query .query-control {
-    width: 210px;
-  }
-
-  .iot-project-info-query .query-control--date {
-    width: 280px;
+    grid-template-columns: repeat(3, minmax(240px, 1fr));
   }
 }
 
@@ -922,12 +914,8 @@ watch(
     );
   }
 
-  .iot-project-info-query .query-actions {
-    width: 100%;
-  }
-
-  .iot-project-info-query .query-actions :deep(.el-form-item__content) {
-    justify-content: flex-start;
+  .iot-project-info-query .query-row {
+    grid-template-columns: repeat(2, minmax(240px, 1fr));
   }
 }
 
@@ -936,15 +924,8 @@ watch(
     padding: 12px;
   }
 
-  .iot-project-info-query .query-row,
-  .iot-project-info-query .query-row :deep(.el-form-item),
-  .iot-project-info-query .query-actions {
-    width: 100%;
-  }
-
-  .iot-project-info-query .query-control,
-  .iot-project-info-query .query-control--date {
-    width: 100%;
+  .iot-project-info-query .query-row {
+    grid-template-columns: minmax(0, 1fr);
   }
 
   .iot-project-info-query .query-actions :deep(.el-form-item__content) {
@@ -961,26 +942,26 @@ watch(
 }
 
 .iot-project-info-query {
-  display: block;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
 }
 
 .query-row {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 12px 24px;
+  display: grid;
+  grid-template-columns: repeat(4, minmax(220px, 1fr)) minmax(320px, 1.4fr);
+  gap: 12px 20px;
   min-width: 0;
 }
 
 .query-actions {
-  margin-left: auto;
-  flex: 0 0 auto;
+  width: 100%;
 }
 
 .query-actions :deep(.el-form-item__content) {
   display: flex;
   flex-wrap: wrap;
-  justify-content: flex-end;
+  justify-content: flex-start;
   gap: 8px 10px;
 }
 
@@ -989,11 +970,11 @@ watch(
 }
 
 .query-control {
-  width: 210px;
+  width: 100%;
 }
 
 .query-control--date {
-  width: 300px;
+  width: 100%;
 }
 
 :deep(.el-form-item) {

Fișier diff suprimat deoarece este prea mare
+ 776 - 707
src/views/pms/iotprojecttask/IotProjectTaskForm.vue


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

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

Fișier diff suprimat deoarece este prea mare
+ 512 - 440
src/views/pms/maintain/IotMaintainAddEdit.vue


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

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

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

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

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

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

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

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

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

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

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

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

+ 58 - 10
src/views/pms/stat/rdkb/rd-rate.vue

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

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

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

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff