yanghao 1 روز پیش
والد
کامیت
dd77da58b2

+ 45 - 41
src/views/pms/iotopeationfill/StatisticsForm.vue

@@ -13,16 +13,14 @@
           :model="queryParams"
           ref="queryFormRef"
           :inline="true"
-          label-width="68px"
-        >
+          label-width="68px">
           <el-form-item label="查询条件" prop="orderName">
             <el-input
               v-model="queryParams.orderName"
               placeholder="请输入查询条件"
               clearable
               @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
+              class="!w-240px" />
           </el-form-item>
           <el-form-item label="创建时间" prop="createTime">
             <el-date-picker
@@ -32,19 +30,21 @@
               start-placeholder="开始日期"
               end-placeholder="结束日期"
               :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-              class="!w-220px"
-            />
+              class="!w-220px" />
           </el-form-item>
           <el-form-item>
-            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+            <el-button @click="handleQuery"
+              ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
+            >
             <el-button
               type="success"
               plain
               @click="handleExport"
               :loading="exportLoading"
-              v-hasPermi="['rq:iot-opeation-fill:export']"
-            >
+              v-hasPermi="['rq:iot-opeation-fill:export']">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -73,20 +73,16 @@
             align="center"
             prop="createTime"
             :formatter="dateFormatter"
-            width="180px"
-          />
+            width="180px" />
         </el-table>
         <Pagination
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+          @pagination="getList" />
       </ContentWrap>
     </el-col>
   </el-row>
-
-
 </template>
 
 <script setup lang="ts">
@@ -94,15 +90,15 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
 import IotOpeationFillForm from './IotOpeationFillForm.vue'
-import * as UserApi from "@/api/system/user";
-import {defaultProps,handleTree} from "@/utils/tree";
-import * as DeptApi from "@/api/system/dept";
-import * as PostApi from "@/api/system/post";
-import {onMounted, ref} from "vue";
-import {DeptTreeItem} from "@/api/system/dept";
-import DeptTree from "@/views/system/user/DeptTree.vue";
-import {useUserStore} from "@/store/modules/user";
-import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
+import * as UserApi from '@/api/system/user'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import { onMounted, ref } from 'vue'
+import { DeptTreeItem } from '@/api/system/dept'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 /** 运行记录填报 列表 */
 defineOptions({ name: 'FillOrderInfo2' })
 const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
@@ -113,14 +109,23 @@ const loading = ref(true) // 列表的加载中
 const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const { params, name } = useRoute() // 查询参数
-const deptId = params.deptId;
-const isFill = params.isFill;
-const createTime = params.createTime;
+const deptId = params.deptId
+const isFill = params.isFill
+const createTime = params.createTime
+const normalizeRouteCreateTime = (value: unknown) => {
+  if (Array.isArray(value)) {
+    return value.filter((item): item is string => typeof item === 'string' && item)
+  }
+  if (typeof value === 'string') {
+    return value.split(',').filter(Boolean)
+  }
+  return []
+}
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const selectedDeptId = ref(null);  // 通过store存储的部门id 由父组件传递过来
+const selectedDeptId = ref(null) // 通过store存储的部门id 由父组件传递过来
 
 const queryParams = reactive({
   pageNo: 1,
@@ -138,7 +143,7 @@ const queryParams = reactive({
   teamName: undefined,
   dutyName: undefined,
   creDate: [],
-  createTime: [],
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -158,18 +163,16 @@ const getList = async () => {
   }
 }
 
-
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = type
   formType.value = type
-  queryParams.deptId = id;
+  queryParams.deptId = id
   // 修改时,设置数据
   if (id) {
-    getList();
+    getList()
   }
-
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -181,7 +184,7 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryParams.deptId = useUserStore().getUser.deptId;
+  queryParams.deptId = useUserStore().getUser.deptId
   queryParams.createTime = null
   queryFormRef.value.resetFields()
   handleQuery()
@@ -224,19 +227,20 @@ const handleExport = async () => {
 /** 初始化 **/
 onMounted(async () => {
   if (deptId) {
-    queryParams.deptId = deptId;
+    queryParams.deptId = deptId
   }
   if (isFill == 2) {
-    queryParams.isFill = null;
+    queryParams.isFill = null
   } else {
     if (isFill) {
-      queryParams.isFill = isFill;
+      queryParams.isFill = isFill
     }
   }
-  if(createTime){
-    queryParams.createTime = createTime;
+  const routeCreateTime = normalizeRouteCreateTime(createTime)
+  if (routeCreateTime.length) {
+    queryParams.createTime = routeCreateTime
   }
   getList()
-  deptList.value = handleTree(await DeptApi.getSimpleDeptList());
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
 })
 </script>

+ 443 - 387
src/views/pms/iotopeationfill/index.vue

@@ -1,334 +1,346 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap style="border: none">
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('operationFill.name')" prop="orderName" style="margin-left: 15px">
-            <el-input
-              v-model="queryParams.orderName"
-              :placeholder="t('operationFill.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('operationFill.status')" prop="orderStatus">
-            <el-select
-              v-model="queryParams.orderStatus"
-              :placeholder="t('operationFill.status')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item
-            :label="t('operationFill.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-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>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap style="border: none">
-        <zm-table :loading="loading" :data="list" height="calc(85vh - 175px)">
-          <zm-table-column :label="t('common.index')" min-width="60" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('bomList.name')" align="center" prop="orderName" />
-          <zm-table-column :label="t('operationFill.duty')" align="center" prop="userName" />
-          <zm-table-column :label="t('operationFill.orderDevice')" align="center" prop="fillList" />
-
-          <zm-table-column :label="t('operationFill.status')" align="center" prop="orderStatus">
-            <template #default="scope">
-              <el-tooltip
-                v-if="scope.row.orderStatus === 3"
-                effect="dark"
-                :content="scope.row.reason"
-                placement="top"
-              >
-                <dict-tag
-                  :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
-                  :value="scope.row.orderStatus"
-                />
-              </el-tooltip>
-              <dict-tag
-                v-else
-                :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
-                :value="scope.row.orderStatus"
-              />
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('operationFill.deviceCount')" align="center" prop="allDev">
-            <template #default="scope">
-              <el-tag type="info"> {{ scope.row.allDev }}</el-tag>
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('operationFill.fillCount')" align="center" prop="fillDev">
-            <template #default="scope">
-              <el-tag type="success"> {{ scope.row.fillDev }}</el-tag>
-            </template>
-          </zm-table-column>
-          <zm-table-column :label="t('operationFill.unFillCount')" align="center" prop="unFillDev">
-            <template #default="scope">
-              <el-tag type="danger"> {{ scope.row.unFillDev }}</el-tag>
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('dict.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            min-width="170"
-          />
-          <zm-table-column
-            :label="t('dict.fillTime')"
-            align="center"
-            prop="updateTime"
-            :formatter="dateFormatter"
-            min-width="170"
-          />
-          <zm-table-column
-            :label="t('operationFill.operation')"
-            align="center"
-            min-width="120px"
-            fixed="right"
-            action
-          >
-            <template #default="scope">
-              <div v-if="scope.row.orderStatus == 0 || scope.row.orderStatus == 2">
-                <el-button
-                  link
-                  type="primary"
-                  @click="
-                    openWrite(
-                      scope.row.deptId +
-                        ',' +
-                        scope.row.userId +
-                        ',' +
-                        scope.row.createTime +
-                        ',' +
-                        scope.row.id +
-                        ',' +
-                        scope.row.orderStatus
-                    )
-                  "
-                  v-hasPermi="['rq:iot-opeation-fill:update']"
-                  v-if="scope.row.orderStatus !== 1"
-                >
-                  {{ t('operationFill.fill') }}
-                </el-button>
-                <el-button
-                  link
-                  type="warning"
-                  v-hasPermi="['rq:iot-opeation-fill:update']"
-                  v-if="scope.row.orderStatus !== 1"
-                  @click="openDialog(scope.row.id)"
-                >
-                  {{ t('operationFill.ignore') }}
-                </el-button>
-              </div>
-              <div v-else-if="scope.row.orderStatus === 3">
-                <el-button
-                  link
-                  type="success"
-                  @click="
-                    openWrite(
-                      scope.row.deptId +
-                        ',' +
-                        scope.row.userId +
-                        ',' +
-                        scope.row.createTime +
-                        ',' +
-                        scope.row.id +
-                        ',' +
-                        scope.row.orderStatus
-                    )
-                  "
-                >
-                  {{ t('operationFill.view') }}
-                </el-button>
-              </div>
-              <div v-else>
-                <el-button
-                  link
-                  type="primary"
-                  @click="
-                    openWrite(
-                      scope.row.deptId +
-                        ',' +
-                        scope.row.userId +
-                        ',' +
-                        scope.row.createTime +
-                        ',' +
-                        scope.row.id +
-                        ',' +
-                        0
-                    )
-                  "
-                  v-hasPermi="['rq:iot-opeation-fill:update']"
-                  v-if="isSameDay(scope.row.createTime)"
-                >
-                  {{ t('fault.edit') }}
-                </el-button>
-                <el-button
-                  link
-                  type="success"
-                  @click="
-                    openWrite(
-                      scope.row.deptId +
-                        ',' +
-                        scope.row.userId +
-                        ',' +
-                        scope.row.createTime +
-                        ',' +
-                        scope.row.id +
-                        ',' +
-                        scope.row.orderStatus
-                    )
-                  "
-                >
-                  {{ t('operationFill.view') }}
-                </el-button>
-              </div>
-
-              <!-- 编辑按钮 -->
-            </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>
+  <div
+    class="operation-fill-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"
+      :dept-id="queryParams.deptId ?? userDeptId"
+      :top-id="rootDeptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="operation-fill-tree row-span-2"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="operation-fill-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('operationFill.name')" prop="orderName">
+          <el-input
+            v-model="queryParams.orderName"
+            :placeholder="t('operationFill.nameHolder')"
+            clearable
+            @keyup.enter="handleQuery"
+            class="query-control" />
+        </el-form-item>
+        <el-form-item :label="t('operationFill.status')" prop="orderStatus">
+          <el-select
+            v-model="queryParams.orderStatus"
+            :placeholder="t('operationFill.status')"
+            clearable
+            class="query-control">
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('operationFill.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-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('common.index')" width="70" align="center" fixed="left">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('bomList.name')"
+                align="center"
+                prop="orderName"
+                fixed="left" />
+              <ZmTableColumn :label="t('operationFill.duty')" align="center" prop="userName" />
+              <ZmTableColumn
+                :label="t('operationFill.orderDevice')"
+                align="center"
+                prop="fillList" />
+
+              <ZmTableColumn :label="t('operationFill.status')" align="center" prop="orderStatus">
+                <template #default="scope">
+                  <el-tooltip
+                    v-if="scope.row.orderStatus === 3"
+                    effect="dark"
+                    :content="scope.row.reason"
+                    placement="top">
+                    <dict-tag
+                      :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
+                      :value="scope.row.orderStatus" />
+                  </el-tooltip>
+                  <dict-tag
+                    v-else
+                    :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
+                    :value="scope.row.orderStatus" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('operationFill.deviceCount')" align="center" prop="allDev">
+                <template #default="scope">
+                  <el-tag type="info"> {{ scope.row.allDev }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn :label="t('operationFill.fillCount')" align="center" prop="fillDev">
+                <template #default="scope">
+                  <el-tag type="success"> {{ scope.row.fillDev }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('operationFill.unFillCount')"
+                align="center"
+                prop="unFillDev">
+                <template #default="scope">
+                  <el-tag type="danger"> {{ scope.row.unFillDev }}</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                :label="t('dict.createTime')"
+                align="center"
+                prop="createTime"
+                :formatter="dateFormatter"
+                min-width="170" />
+              <ZmTableColumn
+                :label="t('dict.fillTime')"
+                align="center"
+                prop="updateTime"
+                :formatter="dateFormatter"
+                min-width="170" />
+              <ZmTableColumn
+                :label="t('operationFill.operation')"
+                align="center"
+                min-width="120px"
+                fixed="right"
+                action>
+                <template #default="scope">
+                  <div v-if="scope.row.orderStatus == 0 || scope.row.orderStatus == 2">
+                    <el-button
+                      link
+                      type="primary"
+                      @click="
+                        openWrite(
+                          scope.row.deptId +
+                            ',' +
+                            scope.row.userId +
+                            ',' +
+                            scope.row.createTime +
+                            ',' +
+                            scope.row.id +
+                            ',' +
+                            scope.row.orderStatus
+                        )
+                      "
+                      v-hasPermi="['rq:iot-opeation-fill:update']"
+                      v-if="scope.row.orderStatus !== 1">
+                      {{ t('operationFill.fill') }}
+                    </el-button>
+                    <el-button
+                      link
+                      type="warning"
+                      v-hasPermi="['rq:iot-opeation-fill:update']"
+                      v-if="scope.row.orderStatus !== 1"
+                      @click="openDialog(scope.row.id)">
+                      {{ t('operationFill.ignore') }}
+                    </el-button>
+                  </div>
+                  <div v-else-if="scope.row.orderStatus === 3">
+                    <el-button
+                      link
+                      type="success"
+                      @click="
+                        openWrite(
+                          scope.row.deptId +
+                            ',' +
+                            scope.row.userId +
+                            ',' +
+                            scope.row.createTime +
+                            ',' +
+                            scope.row.id +
+                            ',' +
+                            scope.row.orderStatus
+                        )
+                      ">
+                      {{ t('operationFill.view') }}
+                    </el-button>
+                  </div>
+                  <div v-else>
+                    <el-button
+                      link
+                      type="primary"
+                      @click="
+                        openWrite(
+                          scope.row.deptId +
+                            ',' +
+                            scope.row.userId +
+                            ',' +
+                            scope.row.createTime +
+                            ',' +
+                            scope.row.id +
+                            ',' +
+                            0
+                        )
+                      "
+                      v-hasPermi="['rq:iot-opeation-fill:update']"
+                      v-if="isSameDay(scope.row.createTime)">
+                      {{ t('fault.edit') }}
+                    </el-button>
+                    <el-button
+                      link
+                      type="success"
+                      @click="
+                        openWrite(
+                          scope.row.deptId +
+                            ',' +
+                            scope.row.userId +
+                            ',' +
+                            scope.row.createTime +
+                            ',' +
+                            scope.row.id +
+                            ',' +
+                            scope.row.orderStatus
+                        )
+                      ">
+                      {{ t('operationFill.view') }}
+                    </el-button>
+                  </div>
+
+                  <!-- 编辑按钮 -->
+                </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>
-  <!-- 表单弹窗:添加/修改 -->
-  <IotInspectOrderForm ref="formRef" @success="getList" />
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
+  <el-dialog
+    v-model="dialogVisible"
+    title="忽略理由"
+    :width="600"
+    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
+          v-model="form.reason"
+          type="textarea"
+          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 type { FormInstance } from 'element-plus'
 import { dateFormatter } from '@/utils/formatTime'
-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 { onMounted, ref } from 'vue'
 import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
 import { useUserStore } from '@/store/modules/user'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotOpeationFillVO>()
 const { push } = useRouter()
 const { query } = useRoute() // 查询参数
-const deptId = query.deptId
-const orderStatus = query.orderStatus
-const createTime = query.createTime
+const getQueryValue = (value: (typeof query)[string]) => (Array.isArray(value) ? value[0] : value)
+const deptId = getQueryValue(query.deptId)
+const orderStatus = getQueryValue(query.orderStatus)
+const createTime = getQueryValue(query.createTime)
+const rootDeptId = 156
+const userDeptId = useUserStore().getUser.deptId
+
+interface QueryParams extends PageParam {
+  orderName?: string
+  inspectOrderTitle?: string
+  inspectOrderCode?: string
+  status?: string
+  remark?: string
+  createTime: string[]
+  deptId?: number | string
+  deviceIds?: string
+  orderStatus?: string | number | null
+}
+
+interface IgnoreForm {
+  id?: number
+  reason: string
+}
 
 /** 巡检工单 列表 */
 defineOptions({ name: 'IotOpeationFill1' })
 const dialogVisible = ref(false)
-const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-let isLeftContentCollapsed = ref(false)
 const loading = ref(true) // 列表的加载中
 const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
+  orderName: undefined,
   inspectOrderTitle: undefined,
   inspectOrderCode: undefined,
   status: undefined,
   remark: undefined,
   createTime: [],
-  deptId: useUserStore().getUser.deptId,
+  deptId: userDeptId,
   deviceIds: undefined,
   orderStatus: undefined
   //userId:useUserStore().getUser.id
 })
 
-const form = reactive({
+const form = reactive<IgnoreForm>({
   id: undefined,
   reason: ''
 })
@@ -340,6 +352,7 @@ const rules = {
     { min: 2, message: '理由长度不能少于2个字符', trigger: 'blur' }
   ]
 }
+
 // 打开对话框
 const openDialog = (id: number) => {
   dialogVisible.value = true
@@ -347,10 +360,6 @@ const openDialog = (id: number) => {
   form.reason = ''
 }
 
-const truncateText = (text: string, maxLength: number) => {
-  if (!text) return ''
-  return text.length > maxLength ? text.substring(0, maxLength) + '...' : text
-}
 // 取消按钮处理
 const handleCancel = () => {
   dialogVisible.value = false
@@ -361,12 +370,13 @@ const handleCancel = () => {
 const handleConfirm = async () => {
   // 表单验证
   try {
-    await reasonFormRef.value.validate()
+    await reasonFormRef.value?.validate()
     // 验证通过,调用接口
     await IotOpeationFillApi.updateIotOpeationFill1(form)
     ElMessage.success('操作成功')
     dialogVisible.value = false
     resetForm()
+    await getList()
   } catch (error) {
     return
   }
@@ -375,36 +385,12 @@ const handleConfirm = async () => {
 const resetForm = () => {
   reasonFormRef.value?.resetFields()
 }
-const reasonFormRef = ref(null)
+const reasonFormRef = ref<FormInstance>()
 
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-
-// 新增变量用于控制悬浮提示
-const hoverRowId = ref<number | null>(null)
-
-// 鼠标进入表格单元格时
-const handleMouseEnter = (row: IotOpeationFillVO) => {
-  hoverRowId.value = row.id
-}
-
-// 鼠标离开表格单元格时
-const handleMouseLeave = () => {
-  hoverRowId.value = null
-}
-
-// 定义要显示的悬浮内容
-const getTooltipContent = (row: IotOpeationFillVO) => {
-  // 根据实际需求自定义显示内容
-  return `
-    ${t('operationFill.name')}: ${row.orderName || '-'}\n
-    ${t('operationFill.duty')}: ${row.userName || '-'}\n
-    ${t('dict.createTime')}: ${row.createTime ? dateFormatter(row.createTime) : '-'}
-  `
-}
 
 // 判断两个日期是否为同一天
-const isSameDay = (dateString) => {
+const isSameDay = (dateString?: string) => {
   if (!dateString) return false
 
   // 将日期字符串转换为日期对象
@@ -419,8 +405,9 @@ const isSameDay = (dateString) => {
   )
 }
 
-const handleDeptNodeClick = async (row) => {
+const handleDeptNodeClick = async (row: Tree) => {
   queryParams.deptId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 /** 查询列表 */
@@ -443,45 +430,22 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (id?: number) => {
-  push({ name: 'InspectOrderDetail', params: { id } })
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
 }
 
-const openWrite = (id?: string) => {
-  push({ name: 'FillOrderInfo', params: { id } })
-}
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotInspectOrderApi.deleteIotInspectOrder(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
 }
 
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotInspectOrderApi.exportIotInspectOrder(queryParams)
-    download.excel(data, '巡检工单.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
+const openWrite = (id?: string) => {
+  push({ name: 'FillOrderInfo', params: { id } })
 }
 
 /** 初始化 **/
@@ -492,7 +456,7 @@ onMounted(async () => {
   start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000)
 
   // 格式化日期为后端需要的格式
-  const formatDate = (date) => {
+  const formatDate = (date: Date) => {
     const year = date.getFullYear()
     const month = String(date.getMonth() + 1).padStart(2, '0')
     const day = String(date.getDate()).padStart(2, '0')
@@ -507,10 +471,10 @@ onMounted(async () => {
   if (deptId != null) {
     queryParams.deptId = deptId
   }
-  if (orderStatus === 4) {
+  if (orderStatus === '4') {
     queryParams.orderStatus = null
   }
-  if (orderStatus != null && orderStatus != 4) {
+  if (orderStatus != null && orderStatus !== '4') {
     queryParams.orderStatus = orderStatus
   }
   if (createTime) {
@@ -528,31 +492,123 @@ onMounted(async () => {
 })
 </script>
 
-<style lang="scss">
-.scrollable-tooltip {
-  min-height: 100px;
-  overflow-y: auto;
-  overflow-x: hidden;
-  /* white-space: pre-wrap;
-  word-break: break-word; */
-  background-color: rgba(0, 0, 0, 0.8);
-  color: #fff;
-  font-size: 14px;
+<style scoped>
+.operation-fill-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
 }
-.project-popover.el-popover.el-popper {
-  opacity: 0.8;
-  background: #000000;
-  border: none !important;
-  font-size: 14px;
-  color: #ffffff;
-  font-weight: 400;
-  min-width: 54px;
-
-  .el-popper__arrow::before {
-    background: #000000;
-    border: none;
+
+.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--date {
+  width: 220px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2200px) {
+  .operation-fill-query,
+  .query-row {
+    flex-wrap: nowrap;
   }
 }
-</style>
 
-<style scoped lang="scss"></style>
+@media (width <= 1500px) {
+  .operation-fill-query {
+    gap: 12px 18px;
+  }
+
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 168px;
+  }
+
+  .query-control--date {
+    width: 210px;
+  }
+}
+
+@media (width <= 1200px) {
+  .operation-fill-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(.operation-fill-tree) {
+    grid-row: auto !important;
+    width: 100% !important;
+    height: 320px !important;
+    min-width: 0 !important;
+  }
+
+  .query-actions {
+    width: 100%;
+  }
+}
+
+@media (width <= 768px) {
+  .operation-fill-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+    margin-left: 0;
+  }
+}
+</style>

+ 373 - 238
src/views/pms/iotopeationfill/plan/IotOperationPlan.vue

@@ -1,165 +1,153 @@
 <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">
+  <div v-loading="formLoading" class="operation-plan-page">
+    <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-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          size="default"
+          label-position="top"
+          class="operation-plan-form">
+          <div class="operation-plan-form__grid">
+            <div class="form-cell">
               <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">
+            </div>
+            <div class="form-cell">
               <el-form-item :label="t('workOrderMaterial.unit')" prop="planUnit">
                 <el-select
                   v-model="formData.planUnit"
                   :placeholder="t('route.unitHolder')"
                   :disabled="formType === 'update'"
                   clearable
-                >
+                  class="w-full">
                   <el-option
                     v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_UNIT)"
                     :key="dict.label"
                     :label="dict.label"
-                    :value="dict.value"
-                  />
+                    :value="dict.value" />
                 </el-select>
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+            <div class="form-cell">
               <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"
-                />
+                  class="!w-full" />
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+
+            <div class="form-cell form-cell--start-time">
               <el-form-item :label="t('route.beginCreateTime')" prop="beginCreateTime">
                 <el-date-picker
-                  style="width: 150%"
                   v-model="formData.beginCreateTime"
                   type="datetime"
                   value-format="x"
-                />
+                  class="!w-full" />
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+            <div class="form-cell form-cell--report">
               <el-form-item label="日报填报" prop="isReport">
                 <el-radio-group v-model="formData.isReport">
                   <el-radio
                     v-for="dict in getIntDictOptions(DICT_TYPE.RQ_IOT_ISCOLLECTION)"
                     :key="dict.value"
-                    :value="dict.value"
-                  >
+                    :value="dict.value">
                     {{ dict.label }}
                   </el-radio>
                 </el-radio-group>
               </el-form-item>
-            </el-col>
-
-              <el-col :span="12">
-                <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>
-      </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.SelectOperationDevice') }}</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>
             </div>
-            <div>
-              <!--              <el-button type="warning" @click="deleteDraggable(index)">{{ t('form.delete') }}</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" />
+            <div class="form-cell form-cell--remark">
+              <el-form-item :label="t('form.remark')" prop="remark">
+                <el-input
+                  v-model="formData.remark"
+                  type="textarea"
+                  :autosize="{ minRows: 4, maxRows: 4 }" />
+              </el-form-item>
+            </div>
+          </div>
+        </el-form>
+      </div>
+    </section>
+
+    <section class="info-card device-card">
+      <div class="info-card__header">
+        <div class="info-card__title">
+          <span class="info-card__marker"></span>
+          <span>设备列表</span>
+        </div>
+        <span class="card-total">共 {{ list.length }} 台设备</span>
+      </div>
+
+      <div class="info-card__body device-card__body">
+        <el-button type="primary" @click="openForm">
+          <Icon icon="ep:plus" class="mr-5px" />
+          {{ t('route.SelectOperationDevice') }}
+        </el-button>
+
+        <el-empty v-if="!list.length" description="暂无设备" class="device-empty" />
+
+        <draggable
+          v-else
+          v-model="list"
+          item-key="id"
+          tag="div"
+          class="sortable-container"
+          handle=".drag-handle"
+          :animation="150"
+          @start="dragStart"
+          @end="dragEnd">
+          <template #item="{ element, index }">
+            <div class="sortable-item">
+              <div class="order-number">{{ index + 1 }}</div>
+              <el-icon class="drag-handle" size="18"><Rank /></el-icon>
+              <div class="component-content">
+                <div class="device-title">{{ element.deviceName || '-' }}</div>
+                <div class="device-meta">
+                  <span>{{ t('iotDevice.code') }}:{{ element.deviceCode || '-' }}</span>
+                </div>
+              </div>
+              <el-button link type="danger" @click="handleDelete(element.id, element.deviceId)">
+                <Icon icon="ep:delete" class="mr-3px" />
                 {{ t('form.delete') }}
               </el-button>
             </div>
-          </div>
-        </template>
-      </draggable>
-    </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>
+      </div>
+    </section>
+
+    <div class="operation-plan-footer">
+      <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
+      <el-button type="primary" :disabled="formLoading" @click="submitForm">
+        {{ t('iotMaintain.save') }}
+      </el-button>
+    </div>
+
     <InspectItemList
       ref="inspectItemFormRef"
       :classify="formData.deviceClassify"
       :deviceId="formData.deviceId"
-      @choose="inspectItemChoose"
-    />
+      @choose="inspectItemChoose" />
     <PlanDeviceList 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'
@@ -167,18 +155,17 @@ import { useUserStore } from '@/store/modules/user'
 import { ref } from 'vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { Rank } from '@element-plus/icons-vue'
 import InspectItemList from '@/views/pms/inspect/route/InspectItemList.vue'
 import PlanDeviceList from '@/views/pms/iotopeationfill/plan/PlanDeviceList.vue'
 import { IotInspectRouteVO } from '@/api/pms/inspect/route'
-import { DICT_TYPE, getStrDictOptions,getIntDictOptions } from '@/utils/dict'
-import { IotInspectPlanApi, IotInspectPlanVO } from '@/api/pms/inspect/plan'
+import { DICT_TYPE, getStrDictOptions, getIntDictOptions } from '@/utils/dict'
 import RouteInspectItemDrawer from '@/views/pms/inspect/plan/RouteInspectItemDrawer.vue'
 import draggable from 'vuedraggable'
-import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
-import {defaultProps,handleTree} from "@/utils/tree";
-import * as ProductClassifyApi from "@/api/pms/productclassify";
-import {IotInspectItemVO} from "@/api/pms/inspect/item";
-import {IotOperationPlanApi, IotOperationPlanVO} from "@/api/pms/iotopeationfill/plan";
+import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import { handleTree } from '@/utils/tree'
+import * as ProductClassifyApi from '@/api/pms/productclassify'
+import { IotOperationPlanApi, IotOperationPlanVO } from '@/api/pms/iotopeationfill/plan'
 
 /** 维修工单 表单 */
 defineOptions({ name: 'OperationPlanAdd' })
@@ -211,7 +198,7 @@ const formData = ref({
   deviceIds: undefined,
   remark: undefined,
   deptId: undefined,
-  planDevList:[]
+  planDevList: []
 })
 const formRules = reactive({
   planTitle: [{ required: true, message: '运行计划标题不能为空', trigger: 'blur' }],
@@ -254,7 +241,7 @@ const queryParams = reactive({
   templateJson: undefined,
   creator: undefined,
   sortingFields: [],
-  assetClass: undefined,
+  assetClass: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 /** 重置按钮操作 */
@@ -275,140 +262,138 @@ const selectedRows = ref<IotDeviceVO[]>([]) // 多选数据(存储所有选中
 const selectAll = async () => {
   try {
     // 显示加载状态
-    loading.value = true;
-    isAllSelected.value = true;
-    deselectedRows.value = [];
-    selectedRows.value = []; // 清空现有选中项
-    allDataCache.value = []; // 清空缓存
+    loading.value = true
+    isAllSelected.value = true
+    deselectedRows.value = []
+    selectedRows.value = [] // 清空现有选中项
+    allDataCache.value = [] // 清空缓存
 
-    const paramsCopy = { ...queryParams };
-    const originalPageNo = paramsCopy.pageNo;
-    const originalPageSize = paramsCopy.pageSize;
+    const paramsCopy = { ...queryParams }
+    const originalPageNo = paramsCopy.pageNo
+    const originalPageSize = paramsCopy.pageSize
 
-    paramsCopy.pageNo = 1;
-    paramsCopy.pageSize = 100; // 按后台限制的最大页数获取
+    paramsCopy.pageNo = 1
+    paramsCopy.pageSize = 100 // 按后台限制的最大页数获取
 
     // 获取第一页数据
-    const firstPageData = await IotDeviceApi.getPlanDevicePage(paramsCopy);
-    totalSelectedCount.value = firstPageData.total;
-    allDataCache.value.push(...firstPageData.list);
+    const firstPageData = await IotDeviceApi.getPlanDevicePage(paramsCopy)
+    totalSelectedCount.value = firstPageData.total
+    allDataCache.value.push(...firstPageData.list)
 
     // 分批获取剩余数据
     if (firstPageData.total > 100) {
-      const totalPages = Math.ceil(firstPageData.total / 100);
-      const fetchPromises = [];
+      const totalPages = Math.ceil(firstPageData.total / 100)
+      const fetchPromises = []
 
       for (let page = 2; page <= totalPages; page++) {
-        fetchPromises.push(IotDeviceApi.getPlanDevicePage({ ...paramsCopy, pageNo: page }));
+        fetchPromises.push(IotDeviceApi.getPlanDevicePage({ ...paramsCopy, pageNo: page }))
       }
 
-      const remainingPages = await Promise.all(fetchPromises);
-      remainingPages.forEach(pageData => {
-        allDataCache.value.push(...pageData.list);
-      });
+      const remainingPages = await Promise.all(fetchPromises)
+      remainingPages.forEach((pageData) => {
+        allDataCache.value.push(...pageData.list)
+      })
     }
 
     // 全选时,将所有数据放入selectedRows
-    selectedRows.value = [...allDataCache.value];
+    selectedRows.value = [...allDataCache.value]
 
     // 恢复原始分页参数
-    queryParams.pageNo = originalPageNo;
-    queryParams.pageSize = originalPageSize;
+    queryParams.pageNo = originalPageNo
+    queryParams.pageSize = originalPageSize
 
-    message.success(t('operationFill.allSelected', { count: totalSelectedCount.value }));
+    message.success(t('operationFill.allSelected', { count: totalSelectedCount.value }))
   } catch (error) {
-    console.error('全选失败', error);
-    message.error(t('operationFill.selectAllFailed'));
-    isAllSelected.value = false;
-    selectedRows.value = [];
+    console.error('全选失败', error)
+    message.error(t('operationFill.selectAllFailed'))
+    isAllSelected.value = false
+    selectedRows.value = []
   } finally {
-    loading.value = false;
+    loading.value = false
   }
-};
+}
 
 // 修改取消全选方法,同步清空selectedRows
 const deselectAll = () => {
-  isAllSelected.value = false;
-  deselectedRows.value = [];
-  selectedRows.value = []; // 清空所有选中项
-  allDataCache.value = []; // 清空缓存
-  totalSelectedCount.value = 0;
-  message.info(t('operationFill.allDeselected'));
-};
+  isAllSelected.value = false
+  deselectedRows.value = []
+  selectedRows.value = [] // 清空所有选中项
+  allDataCache.value = [] // 清空缓存
+  totalSelectedCount.value = 0
+  message.info(t('operationFill.allDeselected'))
+}
 
 // 修改单选逻辑,确保selectedRows正确更新
 const selectRow = (row) => {
   if (isAllSelected.value) {
     // 全选状态下的单选操作
     const deselectedIndex = deselectedRows.value.findIndex(
-      item => item.id === row.id && item.deviceCode === row.deviceCode
-    );
+      (item) => item.id === row.id && item.deviceCode === row.deviceCode
+    )
 
     const selectedIndex = selectedRows.value.findIndex(
-      item => item.id === row.id && item.deviceCode === row.deviceCode
-    );
+      (item) => item.id === row.id && item.deviceCode === row.deviceCode
+    )
 
     if (deselectedIndex > -1) {
       // 从取消列表中移除,添加到选中列表
-      deselectedRows.value.splice(deselectedIndex, 1);
+      deselectedRows.value.splice(deselectedIndex, 1)
       if (selectedIndex === -1) {
-        selectedRows.value.push({ ...row });
+        selectedRows.value.push({ ...row })
       }
     } else {
       // 从选中列表中移除,添加到取消列表
       if (selectedIndex > -1) {
-        selectedRows.value.splice(selectedIndex, 1);
+        selectedRows.value.splice(selectedIndex, 1)
       }
-      deselectedRows.value.push({ ...row });
+      deselectedRows.value.push({ ...row })
     }
   } else {
     // 非全选状态下的单选操作
     const index = selectedRows.value.findIndex(
-      item => item.id === row.id && item.deviceCode === row.deviceCode
-    );
+      (item) => item.id === row.id && item.deviceCode === row.deviceCode
+    )
 
     if (index > -1) {
       // 取消选择
-      selectedRows.value.splice(index, 1);
+      selectedRows.value.splice(index, 1)
     } else {
       // 选中
-      selectedRows.value.push({ ...row });
+      selectedRows.value.push({ ...row })
     }
   }
-};
-
-
+}
 
 // 生成行唯一标识
 const rowKey = (row: any) => {
-  return `${row.id}`; // 确保行更新时重新渲染
-};
+  return `${row.id}` // 确保行更新时重新渲染
+}
 
 // 修改移除标签方法,确保selectedRows同步更新
 const removeTag = (route) => {
   // 从选中列表中移除
   const selectedIndex = selectedRows.value.findIndex(
-    item => item.id === route.id && item.deviceCode === route.deviceCode
-  );
+    (item) => item.id === route.id && item.deviceCode === route.deviceCode
+  )
 
   if (selectedIndex > -1) {
-    selectedRows.value.splice(selectedIndex, 1);
+    selectedRows.value.splice(selectedIndex, 1)
   }
 
   // 全选状态下还要添加到取消列表
   if (isAllSelected.value) {
     const deselectedIndex = deselectedRows.value.findIndex(
-      item => item.id === route.id && item.deviceCode === route.deviceCode
-    );
+      (item) => item.id === route.id && item.deviceCode === route.deviceCode
+    )
 
     if (deselectedIndex === -1) {
-      deselectedRows.value.push({ ...route });
+      deselectedRows.value.push({ ...route })
     }
   }
 
   // 刷新表格
-  list.value = [...list.value];
-};
+  list.value = [...list.value]
+}
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
@@ -427,12 +412,10 @@ const getList = async () => {
 // 拖动状态管理
 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))
     }
   })
   // list.value = rows
@@ -481,16 +464,17 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    formData.value.planDevList = list;
+    formData.value.planDevList = list
     const user = wsCache.get(CACHE_KEY.USER)
     const data = formData.value as unknown as IotOperationPlanVO
     if (formType.value === 'create') {
       //校验是否是已有计划
       const newitems = itemsWithIndex
       formData.value.deviceIds = JSON.stringify(newitems.value)
-      if (!formData.value.deptId) {formData.value.deptId = user.user.deptId}
-      await IotOperationPlanApi.createIotOperationPlan(data).then((res) => {
-      })
+      if (!formData.value.deptId) {
+        formData.value.deptId = user.user.deptId
+      }
+      await IotOperationPlanApi.createIotOperationPlan(data).then((res) => {})
       message.success(t('common.createSuccess'))
       close()
     } else {
@@ -510,7 +494,6 @@ const submitForm = async () => {
 
 /** 重置表单 */
 onMounted(async () => {
-
   const route = useRoute()
   const deptId = useUserStore().getUser.deptId
   formData.value.deptId = Number(route.query.deptId)
@@ -532,8 +515,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)
@@ -542,87 +524,182 @@ const handleDelete = async (id: number, deviceId: number) => {
 }
 </script>
 <style scoped>
-.base-expandable-content {
-  overflow: hidden; /* 隐藏溢出的内容 */
-  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+.operation-plan-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
 }
-/* 横向布局容器 */
 
+.info-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 10px 28px rgb(31 45 61 / 6%);
+}
 
-/* 拖拽手柄样式 */
-.drag-handle {
-  opacity: 0.5;
-  cursor: move;
-  transition: opacity 0.3s;
+.device-card {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  min-height: 360px;
 }
-.drag-handle:hover {
-  opacity: 1;
+
+.info-card__header {
+  display: flex;
+  min-height: 58px;
+  padding: 0 20px;
+  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;
+  justify-content: space-between;
 }
 
-/* 拖拽时的悬停效果 */
-.horizontal-item:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+.info-card__title {
+  display: flex;
+  gap: 10px;
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  align-items: center;
 }
 
-/* 滚动条样式 */
-.horizontal-list::-webkit-scrollbar {
-  height: 8px;
+.info-card__marker {
+  width: 4px;
+  height: 18px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
 }
-.horizontal-list::-webkit-scrollbar-thumb {
-  background: #888;
-  border-radius: 4px;
+
+.info-card__body {
+  padding: 20px;
 }
 
-.sortable-container {
-  cursor: move;
+.card-total {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+}
+
+.operation-plan-form__grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 18px 32px;
+  align-items: start;
+}
+
+.form-cell {
+  min-width: 0;
+}
+
+.form-cell--start-time {
+  grid-column: 1;
+  grid-row: 2;
+}
+
+.form-cell--report {
+  grid-column: 1;
+  grid-row: 3;
+}
+
+.form-cell--remark {
+  grid-column: 2 / 4;
+  grid-row: 2 / 4;
+}
+
+.operation-plan-form :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.operation-plan-form :deep(.el-form-item__label) {
+  margin-bottom: 6px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.form-cell--remark :deep(.el-form-item),
+.form-cell--remark :deep(.el-form-item__content),
+.form-cell--remark :deep(.el-textarea),
+.form-cell--remark :deep(.el-textarea__inner) {
+  height: 100%;
+}
+
+.device-card__body {
   display: flex;
+  flex: 1;
   flex-direction: column;
-  gap: 9px;
-  //max-height: 80vh;
+  gap: 14px;
+  min-height: 0;
+}
+
+.device-card__body > .el-button {
+  align-self: flex-start;
+}
+
+.device-empty {
+  flex: 1;
+  min-height: 260px;
+}
+
+.sortable-container {
+  display: flex;
+  max-height: calc(100vh - 430px);
+  min-height: 0;
+  padding-right: 4px;
   overflow-y: auto;
-  padding: 7px;
+  flex: 1;
+  flex-direction: column;
+  gap: 10px;
 }
 
 .sortable-item {
   display: flex;
+  gap: 12px;
   align-items: center;
-  padding: 8px;
-  background: #fff;
-  border: 1px solid #ebeef5;
+  min-height: 60px;
+  padding: 12px 14px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
   border-radius: 8px;
   transition:
-    transform 0.3s,
-    box-shadow 0.3s;
+    border-color 0.2s,
+    box-shadow 0.2s,
+    transform 0.2s;
   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-5);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 14px rgb(31 45 61 / 8%);
 }
 
 .order-number {
-  width: 24px;
-  height: 24px;
   display: flex;
+  flex: 0 0 28px;
   align-items: center;
   justify-content: center;
-  background: #409eff;
-  color: white;
+  width: 28px;
+  height: 28px;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
   border-radius: 50%;
-  margin-right: 7px;
-  font-weight: bold;
 }
 
 .drag-handle {
-  padding: 0 12px;
-  opacity: 0.4;
-  transition: opacity 0.3s;
+  flex: 0 0 auto;
+  color: var(--el-text-color-placeholder);
+  cursor: grab;
+  transition: color 0.2s;
 }
 
 .drag-handle:hover {
-  opacity: 1;
+  color: var(--el-color-primary);
 }
 
 .component-content {
@@ -630,12 +707,70 @@ const handleDelete = async (id: number, deviceId: number) => {
   min-width: 0;
 }
 
-/* 优化滚动条 */
+.device-title {
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.device-meta {
+  display: flex;
+  gap: 18px;
+  margin-top: 2px;
+  overflow: hidden;
+  font-size: 13px;
+  line-height: 20px;
+  color: var(--el-text-color-secondary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.operation-plan-footer {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 24px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+}
+
 .sortable-container::-webkit-scrollbar {
   width: 8px;
 }
+
 .sortable-container::-webkit-scrollbar-thumb {
-  background: #c0c4cc;
+  background: var(--el-border-color);
   border-radius: 4px;
 }
+
+@media (width <= 1200px) {
+  .operation-plan-form__grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .form-cell--start-time,
+  .form-cell--report,
+  .form-cell--remark {
+    grid-column: auto;
+    grid-row: auto;
+  }
+
+  .form-cell--remark {
+    grid-column: 1 / -1;
+  }
+}
+
+@media (width <= 768px) {
+  .operation-plan-form__grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .form-cell--remark {
+    grid-column: auto;
+  }
+}
 </style>

+ 258 - 283
src/views/pms/iotopeationfill/plan/index.vue

@@ -1,210 +1,45 @@
-<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-180px"
-            />
-          </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-180px"
-            />
-          </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-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-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>
-
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list" height="calc(85vh - 175px)">
-          <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <zm-table-column :label="t('route.planTitle')" align="center" prop="planTitle" />
-          <!--          <el-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')"
-            prop="status"
-            column-key="status"
-            min-width="80"
-          >
-            <template #default="scope">
-              <el-switch
-                :model-value="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="(value) => handleStatusChange(scope.row, value)"
-              />
-            </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-operation-plan:update']"
-              >
-                {{ t('fault.edit') }}
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-operation-plan:delete']"
-              >
-                {{ t('fault.del') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <!-- 表单弹窗:添加/修改 -->
-  <IotOperationPlanForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue'
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+<script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotOperationPlanApi, IotOperationPlanVO } from '@/api/pms/iotopeationfill/plan'
-import IotOperationPlanForm from './IotOperationPlanForm.vue'
 import { CommonStatusEnum } from '@/utils/constants'
+import { dateFormatter } from '@/utils/formatTime'
 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 { checkPermi } from '@/utils/permission'
+import download from '@/utils/download'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+defineOptions({ name: 'IotOperationPlan' })
 
-defineOptions({ name: 'iotOperationPlan' })
-const message = useMessage() // 消息弹窗
+type IotOperationPlanRow = IotOperationPlanVO & {
+  status?: number
+  beginCreateTime?: string
+  lastCreateTime?: string
+  deptName?: string
+  createName?: string
+}
 
+interface QueryParams extends PageParam {
+  planTitle?: string
+  planCode?: string
+  planCycle?: number
+  planUnit?: string
+  charge?: string
+  deviceIds?: string
+  remark?: string
+  createTime?: string[]
+  deptId?: number
+}
+
+const message = useMessage()
 const { push } = useRouter()
 const { t } = useI18n()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotOperationPlanRow>()
 
 const loading = ref(true)
-const list = ref<IotOperationPlanVO[]>([])
 const total = ref(0)
-const queryParams = reactive({
+const list = ref<IotOperationPlanRow[]>([])
+const exportLoading = ref(false)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   planTitle: undefined,
@@ -218,48 +53,34 @@ const queryParams = reactive({
   deptId: undefined
 })
 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) => {
-  selectedDept.value = { id: row.id, name: row.name }
-  queryParams.deptId = row.id
-  await getList()
+const normalizeStatus = (status: unknown) => {
+  const value = Number(status)
+  if (value === CommonStatusEnum.ENABLE || value === CommonStatusEnum.DISABLE) {
+    return value
+  }
+  return undefined
 }
 
-const handleStatusChange = async (row: IotOperationPlanVO, value: number) => {
-  // 如果还未初始化完成,直接返回
-  if (!isInitialized.value) {
-    return
-  }
-  const previousStatus = row.status
-  if (previousStatus === value) {
-    return
-  }
-  try {
-    const text = value === CommonStatusEnum.ENABLE ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '""' + row.planTitle + '"巡检计划吗?')
-    row.status = value
-    await IotOperationPlanApi.updateIotOperationStatus(row.id, value)
-    await getList()
-  } catch {
-    row.status = previousStatus
-  }
+const isValidStatus = (status: unknown) => normalizeStatus(status) !== undefined
+
+const getStatusText = (status: unknown) => {
+  const value = normalizeStatus(status)
+  if (value === CommonStatusEnum.ENABLE) return '启用'
+  if (value === CommonStatusEnum.DISABLE) return '停用'
+  return '未知'
 }
 
-const isInitialized = ref(false)
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotOperationPlanApi.getIotOperationPlanPage(queryParams)
-    list.value = data.list
+    list.value = data.list.map((item: IotOperationPlanRow) => ({
+      ...item,
+      status: normalizeStatus(item.status)
+    }))
     total.value = data.total
-    setTimeout(() => {
-      isInitialized.value = true
-    }, 100)
   } finally {
     loading.value = false
   }
@@ -271,11 +92,29 @@ const handleQuery = () => {
 }
 
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   selectedDept.value = undefined
+  queryParams.deptId = undefined
   handleQuery()
 }
 
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeptNodeClick = async (row: Tree) => {
+  selectedDept.value = { id: row.id, name: row.name }
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
 const openForm = (type: string, id?: number) => {
   if (typeof id === 'number') {
     push({ name: 'OperationPlanEdit', params: { id } })
@@ -290,6 +129,32 @@ const openForm = (type: string, id?: number) => {
   })
 }
 
+const handleStatusChange = async (
+  row: IotOperationPlanRow,
+  nextStatus: string | number | boolean
+) => {
+  const status = normalizeStatus(nextStatus)
+  const oldStatus = normalizeStatus(row.status)
+  if (
+    !row.id ||
+    !row.planTitle ||
+    status === undefined ||
+    oldStatus === undefined ||
+    status === oldStatus
+  ) {
+    return
+  }
+
+  try {
+    await message.confirm(`确认要"${getStatusText(status)}""${row.planTitle}"巡检计划吗?`)
+    row.status = status
+    await IotOperationPlanApi.updateIotOperationStatus(row.id, status)
+    await getList()
+  } catch {
+    row.status = oldStatus
+  }
+}
+
 const handleDelete = async (id: number) => {
   try {
     await message.delConfirm()
@@ -311,70 +176,180 @@ 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;
-}
-
-.leftcontent.collapsed {
-  width: 0;
-  overflow: hidden;
-}
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      v-model="queryParams.deptId"
+      :dept-id="queryParams.deptId ?? 0"
+      :top-id="0"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="row-span-2"
+      @node-click="handleDeptNodeClick" />
 
-.rightcontent {
-  position: relative;
-}
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-3 gap-6 flex items-center justify-between flex-wrap min-w-0">
+      <div class="flex items-center gap-6 flex-wrap">
+        <el-form-item :label="t('main.planName')" prop="planTitle">
+          <el-input
+            v-model="queryParams.planTitle"
+            :placeholder="t('main.nameHolder')"
+            clearable
+            class="!w-200px"
+            @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="!w-200px"
+            @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="!w-320px" />
+        </el-form-item>
+      </div>
 
-.toggle-button {
-  position: absolute;
-  top: 44%;
-  transform: translate(-65%, -50%);
-  width: 12px;
-  height: 40px;
-  background-color: #f0f0f0;
-  cursor: pointer;
-  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;
-}
+      <el-form-item>
+        <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>
 
-/* 添加鼠标悬停样式 */
-.toggle-button:hover {
-  background-color: #afafaf;
-}
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn prop="planTitle" :label="t('route.planTitle')" fixed="left" />
+              <ZmTableColumn prop="planCycle" :label="t('route.cycle')" width="90" />
+              <ZmTableColumn prop="planUnit" :label="t('workOrderMaterial.unit')" width="90">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_INSPECT_UNIT" :value="row.planUnit" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="status"
+                :label="t('bomList.status')"
+                :real-value="(row) => getStatusText(row.status)"
+                width="90">
+                <template #default="{ row }">
+                  <el-switch
+                    v-if="isValidStatus(row.status)"
+                    :model-value="row.status"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="!checkPermi(['rq:iot-operation-plan:update'])"
+                    @change="(status) => handleStatusChange(row, status)" />
+                  <el-tag v-else type="info">未知</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="beginCreateTime"
+                :label="t('route.beginCreateTime')"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn
+                prop="lastCreateTime"
+                :label="t('route.lastCreateTime')"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn prop="deptName" :label="t('iotDevice.dept')" width="130" />
+              <ZmTableColumn prop="createName" :label="t('inspect.createName')" width="130" />
+              <ZmTableColumn :label="t('operationFill.operation')" width="150" fixed="right" action>
+                <template #default="{ row }">
+                  <div class="flex items-center justify-center gap-2">
+                    <el-button
+                      type="primary"
+                      link
+                      @click="openForm('update', row.id)"
+                      v-hasPermi="['rq:iot-operation-plan:update']">
+                      <Icon icon="ep:edit" class="mr-3px" />{{ t('fault.edit') }}
+                    </el-button>
+                    <el-button
+                      type="danger"
+                      link
+                      @click="handleDelete(row.id)"
+                      v-hasPermi="['rq:iot-operation-plan:delete']">
+                      <Icon icon="ep:delete" class="mr-3px" />{{ t('fault.del') }}
+                    </el-button>
+                  </div>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
 
-.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; /* 移除左边框 */
-}
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+</template>
 
-.triangle.rotated {
-  transform: rotate(180deg);
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
 }
 </style>

+ 765 - 513
src/views/pms/iotopeationfill/statistics.vue

@@ -1,163 +1,164 @@
 <template>
-  <div class="statistics-page">
-  <!-- 第一行:统计卡片行 -->
-  <el-row :gutter="16" class="section-row">
-    <el-col :span="24">
-      <el-card class="chart-card filter-card" shadow="never">
-        <el-form
-          class="filter-form"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <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: 180px"
-            />
-          </el-form-item>
-          <el-form-item label="创建时间" prop="createTime">
-            <el-date-picker
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="daterange"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-              class="!w-220px"
-            />
-          </el-form-item>
-<!--          <el-form-item label="填写状态" prop="project_name">
-            <el-select
-              v-model="queryParams.status"
-              placeholder="填写状态"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>-->
-          <el-form-item class="filter-actions">
-            <el-button class="action-btn" @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-            <el-button class="action-btn" @click="resetQuery"><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="section-row">
-    <el-col :span="12">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="card-header">
-            <span class="card-title">运行记录工单统计</span>
-          </div>
-        </template>
-        <el-row class="gauge-row">
-          <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,4,queryParams.createTime)">
-            <div ref="reportingChartRef" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>总数</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,1,queryParams.createTime)">
-            <div ref="dealFinishedChartRef" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>已填写</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,0,queryParams.createTime)">
-            <div ref="transOrderChartRef" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>未填写</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,2,queryParams.createTime)">
-            <div ref="writeChartRef" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>填写中</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,3,queryParams.createTime)">
-            <div ref="ignoreChartRef" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>忽略</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-    <el-col :span="12">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="card-header">
-            <span class="card-title">运行记录设备统计</span>
-          </div>
-        </template>
-        <el-row class="gauge-row">
-          <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,2,queryParams.createTime)">
-            <div ref="reportingChartRef1" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>总数</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,1,queryParams.createTime)">
-            <div ref="dealFinishedChartRef1" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>已填写</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,0,queryParams.createTime)">
-            <div ref="transOrderChartRef1" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>未填写</span>
-            </div>
-          </el-col>
-          <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,3,queryParams.createTime)">
-            <div ref="ignoreChartRef1" class="h-[160px] w-full"></div>
-            <div class="gauge-label">
-              <span>忽略</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-  </el-row>
-
-
-  <!-- 第三行:消息统计行 -->
-  <el-row class="section-row">
-    <el-col :span="24">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="card-header">
-            <span class="card-title">运行记录工单统计</span>
-          </div>
-        </template>
-        <div ref="chartContainer" class="trend-chart"></div>
-      </el-card>
-    </el-col>
-  </el-row>
+  <div ref="wrapperRef" class="statistics-scale-wrapper bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div class="statistics-page bg" :style="targetAreaStyle">
+        <header class="header">运行记录填报统计</header>
+
+        <main class="statistics-content">
+          <el-row :gutter="16" class="section-row">
+            <el-col :span="24">
+              <el-card class="chart-card filter-card panel" shadow="never">
+                <div class="panel-title panel-title--md">
+                  <div class="icon-decorator">
+                    <span></span>
+                    <span></span>
+                  </div>
+                  筛选条件
+                </div>
+                <el-form
+                  class="filter-form"
+                  :model="queryParams"
+                  ref="queryFormRef"
+                  :inline="true"
+                  label-width="86px">
+                  <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
+                      class="filter-control" />
+                  </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"
+                      :shortcuts="rangeShortcuts"
+                      start-placeholder="开始日期"
+                      end-placeholder="结束日期"
+                      :default-time="datePickerDefaultTime"
+                      class="filter-control filter-control--date" />
+                  </el-form-item>
+                  <el-form-item class="filter-actions">
+                    <el-button class="action-btn" size="default" @click="handleQuery">
+                      <Icon icon="ep:search" class="mr-5px" /> 搜索
+                    </el-button>
+                    <el-button class="action-btn" size="default" @click="resetQuery">
+                      <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="section-row">
+            <el-col :span="12">
+              <el-card class="chart-card panel" shadow="never">
+                <div class="panel-title">
+                  <div class="icon-decorator">
+                    <span></span>
+                    <span></span>
+                  </div>
+                  运行记录工单统计
+                </div>
+                <div class="metric-grid metric-grid--order">
+                  <button
+                    v-for="item in orderMetricCards"
+                    :key="item.key"
+                    type="button"
+                    class="metric-card"
+                    :style="{
+                      '--metric-accent': item.accent,
+                      '--metric-glow': item.glow,
+                      '--metric-progress': `${item.percent}%`
+                    }"
+                    @click="openFill(queryParams.deptId, item.status, queryParams.createTime)">
+                    <span class="metric-card__label">{{ item.label }}</span>
+                    <strong class="metric-card__value">
+                      <count-to
+                        class="metric-card__count"
+                        :start-val="0"
+                        :end-val="item.value"
+                        :duration="800"
+                        separator="" />
+                    </strong>
+                    <span class="metric-card__hint">{{ item.hint }}</span>
+                    <span class="metric-card__progress">
+                      <span></span>
+                    </span>
+                  </button>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="12">
+              <el-card class="chart-card panel" shadow="never">
+                <div class="panel-title">
+                  <div class="icon-decorator">
+                    <span></span>
+                    <span></span>
+                  </div>
+                  运行记录设备统计
+                </div>
+                <div class="metric-grid metric-grid--device">
+                  <button
+                    v-for="item in deviceMetricCards"
+                    :key="item.key"
+                    type="button"
+                    class="metric-card"
+                    :style="{
+                      '--metric-accent': item.accent,
+                      '--metric-glow': item.glow,
+                      '--metric-progress': `${item.percent}%`
+                    }"
+                    @click="openForm(queryParams.deptId, item.status, queryParams.createTime)">
+                    <span class="metric-card__label">{{ item.label }}</span>
+                    <strong class="metric-card__value">
+                      <count-to
+                        class="metric-card__count"
+                        :start-val="0"
+                        :end-val="item.value"
+                        :duration="800"
+                        separator="" />
+                    </strong>
+                    <span class="metric-card__hint">{{ item.hint }}</span>
+                    <span class="metric-card__progress">
+                      <span></span>
+                    </span>
+                  </button>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+
+          <el-row class="section-row trend-row">
+            <el-col :span="24">
+              <el-card class="chart-card trend-card panel" shadow="never">
+                <div class="panel-title">
+                  <div class="icon-decorator">
+                    <span></span>
+                    <span></span>
+                  </div>
+                  部门运行记录工单统计
+                </div>
+                <div ref="chartContainer" class="trend-chart"></div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </main>
+      </div>
+    </div>
   </div>
 </template>
 
 <script setup lang="ts" name="Index">
 import * as echarts from 'echarts/core'
-import { BarChart } from 'echarts/charts'; // 显式导入柱状图模块
+import { BarChart } from 'echarts/charts' // 显式导入柱状图模块
 
 import {
   GridComponent,
@@ -166,24 +167,79 @@ import {
   ToolboxComponent,
   TooltipComponent
 } from 'echarts/components'
-import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
+import { LineChart, PieChart } from 'echarts/charts'
 import { LabelLayout, UniversalTransition } from 'echarts/features'
 import { CanvasRenderer } from 'echarts/renderers'
-import {
-  IotStatisticsDeviceMessageSummaryRespVO,
-  IotStatisticsSummaryRespVO
-} from '@/api/iot/statistics'
-import { formatDate } from '@/utils/formatTime'
 import { IotStatApi } from '@/api/pms/stat'
-import { ref, reactive, onMounted, onUnmounted } from "vue";
-import { defaultProps, handleTree } from "@/utils/tree";
-import * as DeptApi from "@/api/system/dept";
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import {useUserStore} from "@/store/modules/user";
-// 导入部门数据类型
-import { DeptTreeItem } from '@/api/system/dept'
-import StatisticsForm from './StatisticsForm.vue'
+import CountTo from '@/components/count-to1.vue'
+import { computed, ref, reactive, onMounted, onUnmounted } from 'vue'
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import type { DeptVO } from '@/api/system/dept'
+import { useUserStore } from '@/store/modules/user'
+import { rangeShortcuts } from '@/utils/formatTime'
+import dayjs from 'dayjs'
 const { push } = useRouter()
+
+const CHART_COLORS = {
+  blue: '#1d5fbf',
+  blueLight: '#8ec5ff',
+  orange: '#d96b1f',
+  green: '#13936f',
+  red: '#d94b4b',
+  purple: '#5f56d8',
+  split: 'rgba(31, 91, 184, 0.1)',
+  axis: '#24364f'
+}
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
+const datePickerDefaultTime: [Date, Date] = [
+  dayjs().startOf('day').toDate(),
+  dayjs().endOf('day').toDate()
+]
+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 getDefaultTimeRange = () => {
+  const end = dayjs()
+  const start = end.subtract(7, 'day')
+
+  return {
+    startTime: start.valueOf(),
+    endTime: end.valueOf(),
+    createTime: [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
+  }
+}
+
 // 初始化echarts
 echarts.use([
   TooltipComponent,
@@ -196,33 +252,39 @@ echarts.use([
   GridComponent,
   LineChart,
   UniversalTransition,
-  GaugeChart,
   BarChart
 ])
 
 const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
 const deptDataList = ref<DeptDataItem[]>([]) // 部门数据列表
 
+type QueryParams = {
+  startTime: number
+  endTime: number
+  createTime: string[] | null
+  deptId: number | null
+  status: number | null
+}
+
+type DeptTreeItem = DeptVO & {
+  children?: DeptTreeItem[]
+}
+
 // 定义部门数据类型
 interface DeptDataItem {
-  deptId: number;
-  name: string;
-  totalCount: number;
-  filledCount: number;
-  unfilledCount: number;
-  fillingCount: number;
+  deptId: number
+  name: string
+  totalCount: number
+  filledCount: number
+  unfilledCount: number
+  fillingCount: number
 }
 
 /** IoT 首页 */
-defineOptions({ name: 'iotOpeationSta' })
-
-const timeRange = ref('7d') // 修改默认选择为近一周
-const dateRange = ref<[Date, Date] | null>(null)
+defineOptions({ name: 'IotOpeationSta' })
 
-const queryParams = reactive({
-  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
-  endTime: Date.now(), // 设置默认结束时间为当前时间
-  createTime: [],
+const queryParams = reactive<QueryParams>({
+  ...getDefaultTimeRange(),
   deptId: null, // 选中的部门ID
   status: null // 填写状态
 })
@@ -234,16 +296,23 @@ const handleQuery = () => {
   initChart()
 }
 
-const formRef = ref()
-const openForm = (deptId?:string,isFill?:string,createTime?:[]) => {
-  push({ name: 'FillOrderInfo2',params:{deptId,isFill,createTime}})
+const initialQueryParams: QueryParams = { ...queryParams }
+
+const openForm = (deptId?: number | null, isFill?: number, createTime?: string[] | null) => {
+  const params: Record<string, string | string[]> = {}
+  if (deptId != null) params.deptId = String(deptId)
+  if (isFill != null) params.isFill = String(isFill)
+  if (createTime?.length) params.createTime = createTime
+
+  push({ name: 'FillOrderInfo2', params })
 }
-const openFill = (deptId?:string,orderStatus?:string,createTime?:[]) => {
+const openFill = (deptId?: number | null, orderStatus?: number, createTime?: string[] | null) => {
   push({
     name: 'IotOpeationFill1',
-    query: {  // 使用query传递参数,而非params
-      deptId,
-      orderStatus,
+    query: {
+      // 使用query传递参数,而非params
+      deptId: deptId == null ? undefined : String(deptId),
+      orderStatus: orderStatus == null ? undefined : String(orderStatus),
       // 数组可以直接传递,会自动序列化
       createTime: createTime ? createTime.join(',') : undefined
       // 建议将数组转为字符串,避免路由参数格式问题
@@ -251,65 +320,21 @@ const openFill = (deptId?:string,orderStatus?:string,createTime?:[]) => {
   })
 }
 
-
-
 const resetQuery = () => {
+  const defaultTimeRange = getDefaultTimeRange()
+
   // 重置查询参数
-  queryParams.startTime = Date.now() - 7 * 24 * 60 * 60 * 1000
-  queryParams.endTime = Date.now()
-  queryParams.deptId = useUserStore().getUser.deptId;
-  queryParams.createTime = null
+  queryParams.startTime = defaultTimeRange.startTime
+  queryParams.endTime = defaultTimeRange.endTime
+  queryParams.deptId = useUserStore().getUser.deptId ?? null
+  queryParams.createTime = defaultTimeRange.createTime
 
   // 重新获取数据
   getStats()
+  getDevStats()
   initChart()
 }
 
-//运行记录工单统计
-const reportingChartRef = ref() // 在线设备统计的图表
-const dealFinishedChartRef = ref() // 离线设备统计的图表
-const transOrderChartRef = ref() // 待激活设备统计的图表
-const writeChartRef = ref() // 待填写图表
-const ignoreChartRef = ref() // 上下行消息量统计的图表
-//运行记录设备统计
-const reportingChartRef1 = ref() // 在线设备统计的图表
-const dealFinishedChartRef1 = ref() // 离线设备统计的图表
-const transOrderChartRef1 = ref() // 待激活设备统计的图表
-const ignoreChartRef1 = ref() // 上下行消息量统计的图表
-
-// 基础统计数据
-const statsData = ref<IotStatisticsSummaryRespVO>({
-  productCategoryCount: 0,
-  productCount: 0,
-  deviceCount: 0,
-  deviceMessageCount: 0,
-  productCategoryTodayCount: 0,
-  productTodayCount: 0,
-  deviceTodayCount: 0,
-  deviceMessageTodayCount: 0,
-  deviceOnlineCount: 0,
-  deviceOfflineCount: 0,
-  deviceInactiveCount: 0,
-  productCategoryDeviceCounts: {}
-})
-
-const day = ref({
-  failureDay: undefined,
-  maintainDay: undefined
-})
-const week = ref({
-  failureWeek: undefined,
-  maintainWeek: undefined
-})
-const month = ref({
-  failureMonth: undefined,
-  maintainMonth: undefined
-})
-const total = ref({
-  failureTotal: undefined,
-  maintainTotal: undefined
-})
-
 const status = ref<IotStatusItem[]>([])
 const status1 = ref<IotStatusItem[]>([])
 // 定义状态项接口
@@ -318,200 +343,177 @@ interface IotStatusItem {
   filledCount: number
   unfilledCount: number
   fillingCount: number
+  ignoreCount?: number
 }
 
-// 消息统计数据
-const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
-  upstreamCounts: {},
-  downstreamCounts: {}
-})
-
-/** 处理快捷时间范围选择 */
-const handleTimeRangeChange = (timeRange: string) => {
-  const now = Date.now()
-  let startTime: number
-
-  switch (timeRange) {
-    case '1h':
-      startTime = now - 60 * 60 * 1000
-      break
-    case '24h':
-      startTime = now - 24 * 60 * 60 * 1000
-      break
-    case '7d':
-      startTime = now - 7 * 24 * 60 * 60 * 1000
-      break
-    default:
-      return
-  }
-
-  // 清空日期选择器
-  dateRange.value = null
-
-  // 更新查询参数
-  queryParams.startTime = startTime
-  queryParams.endTime = now
+const toFiniteNumber = (value: unknown) => {
+  const numberValue = Number(value)
+  return Number.isFinite(numberValue) ? numberValue : 0
+}
 
-  // 重新获取数据
-  getStats()
-  initChart()
+const getMetricPercent = (value: number, total: number) => {
+  if (!total) return value > 0 ? 100 : 0
+  return Math.min(100, Math.round((value / total) * 100))
 }
 
-/** 处理自定义日期范围选择 */
-const handleDateRangeChange = (value: [Date, Date] | null) => {
-  if (value) {
-    // 清空快捷选项
-    timeRange.value = ''
+const createMetricCard = (
+  key: string,
+  label: string,
+  value: number,
+  total: number,
+  status: number,
+  accent: string,
+  glow: string
+) => ({
+  key,
+  label,
+  value,
+  status,
+  accent,
+  glow,
+  percent: key === 'total' ? 100 : getMetricPercent(value, total),
+  hint: key === 'total' ? '全部记录' : `占比 ${getMetricPercent(value, total)}%`
+})
 
-    // 更新查询参数
-    queryParams.startTime = value[0].getTime()
-    queryParams.endTime = value[1].getTime()
+const orderMetricCards = computed(() => {
+  const firstStatus = status.value[0] || {}
+  const totalCount = toFiniteNumber(firstStatus.totalCount)
+
+  return [
+    createMetricCard(
+      'total',
+      '总数',
+      totalCount,
+      totalCount,
+      4,
+      CHART_COLORS.blue,
+      'rgba(29, 95, 191, 0.16)'
+    ),
+    createMetricCard(
+      'filled',
+      '已填写',
+      toFiniteNumber(firstStatus.filledCount),
+      totalCount,
+      1,
+      CHART_COLORS.green,
+      'rgba(19, 147, 111, 0.16)'
+    ),
+    createMetricCard(
+      'unfilled',
+      '未填写',
+      toFiniteNumber(firstStatus.unfilledCount),
+      totalCount,
+      0,
+      CHART_COLORS.orange,
+      'rgba(217, 107, 31, 0.16)'
+    ),
+    createMetricCard(
+      'filling',
+      '填写中',
+      toFiniteNumber(firstStatus.fillingCount),
+      totalCount,
+      2,
+      CHART_COLORS.red,
+      'rgba(217, 75, 75, 0.15)'
+    ),
+    createMetricCard(
+      'ignore',
+      '忽略',
+      toFiniteNumber(firstStatus.ignoreCount),
+      totalCount,
+      3,
+      CHART_COLORS.purple,
+      'rgba(95, 86, 216, 0.16)'
+    )
+  ]
+})
 
-    // 重新获取数据
-    getStats()
-    initChart()
-  }
-}
+const deviceMetricCards = computed(() => {
+  const firstStatus = status1.value[0] || {}
+  const totalCount = toFiniteNumber(firstStatus.totalCount)
+
+  return [
+    createMetricCard(
+      'total',
+      '总数',
+      totalCount,
+      totalCount,
+      2,
+      CHART_COLORS.blue,
+      'rgba(29, 95, 191, 0.16)'
+    ),
+    createMetricCard(
+      'filled',
+      '已填写',
+      toFiniteNumber(firstStatus.filledCount),
+      totalCount,
+      1,
+      CHART_COLORS.green,
+      'rgba(19, 147, 111, 0.16)'
+    ),
+    createMetricCard(
+      'unfilled',
+      '未填写',
+      toFiniteNumber(firstStatus.unfilledCount),
+      totalCount,
+      0,
+      CHART_COLORS.orange,
+      'rgba(217, 107, 31, 0.16)'
+    ),
+    createMetricCard(
+      'ignore',
+      '忽略',
+      toFiniteNumber(firstStatus.ignoreCount),
+      totalCount,
+      3,
+      CHART_COLORS.purple,
+      'rgba(95, 86, 216, 0.16)'
+    )
+  ]
+})
 
 /** 获取统计数据 */
 const getStats = async () => {
   // 获取基础统计数据
   // 获取部门统计数据
   IotStatApi.getDeptStatistics(queryParams).then((res) => {
-    deptDataList.value = res.deptCountList || [];
-    status.value = res.totalList || [];
+    deptDataList.value = res.deptCountList || []
+    status.value = res.totalList || []
     initChart()
-    initCharts()
   })
 }
 const getDevStats = async () => {
   // 获取基础统计数据
   // 获取部门统计数据
   IotStatApi.getDevSta(queryParams).then((res) => {
-    status1.value = res.totalList || [];
+    status1.value = res.totalList || []
     initChart()
-    initCharts()
   })
 }
 
-/** 初始化图表 */
-const initCharts = () => {
-  // 使用数组的第一个元素(如果存在)
-  const firstStatus = status.value[0] || {}
-  const firstStatus1 = status1.value[0] || {}
-  // 上报中
-  initGaugeChart(
-    reportingChartRef.value,
-    firstStatus.totalCount === undefined ? 0 : firstStatus.totalCount,
-    '#0d9'
-  )
-  initGaugeChart(
-    reportingChartRef1.value,
-    firstStatus1.totalCount === undefined ? 0 : firstStatus1.totalCount,
-    '#0d9'
-  )
-  // 处理完成
-  initGaugeChart(
-    dealFinishedChartRef.value,
-    firstStatus.filledCount === undefined ? 0 : firstStatus.filledCount,
-    '#05b'
-  )
-
-  initGaugeChart(
-    ignoreChartRef.value,
-    firstStatus.ignoreCount === undefined ? 0 : firstStatus.ignoreCount,
-    'purple'
-  )
-
-  initGaugeChart(
-    ignoreChartRef1.value,
-    firstStatus1.ignoreCount === undefined ? 0 : firstStatus1.ignoreCount,
-    'purple'
-  )
-
-  initGaugeChart(
-    dealFinishedChartRef1.value,
-    firstStatus1.filledCount === undefined ? 0 : firstStatus1.filledCount,
-    '#05b'
-  )
-  // 转工单
-  initGaugeChart(
-    transOrderChartRef.value,
-    firstStatus.unfilledCount === undefined ? 0 : firstStatus.unfilledCount,
-    '#f50'
-  )
-  initGaugeChart(
-    transOrderChartRef1.value,
-    firstStatus1.unfilledCount === undefined ? 0 : firstStatus1.unfilledCount,
-    '#f50'
-  )
-  // 待填写
-  initGaugeChart(
-    writeChartRef.value,
-    firstStatus.fillingCount === undefined ? 0 : firstStatus.fillingCount,
-    '#05b'
-  )
-}
-
-/** 初始化仪表盘图表 */
-const initGaugeChart = (el: any, value: number, color: string) => {
-  echarts.init(el).setOption({
-    series: [
-      {
-        type: 'gauge',
-        startAngle: 360,
-        endAngle: 0,
-        min: 0,
-        max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
-        progress: {
-          show: true,
-          width: 12,
-          itemStyle: {
-            color: color
-          }
-        },
-        axisLine: {
-          lineStyle: {
-            width: 12,
-            color: [[1, '#E5E7EB']]
-          }
-        },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        pointer: { show: false },
-        anchor: { show: false },
-        title: { show: false },
-        detail: {
-          valueAnimation: true,
-          fontSize: 24,
-          fontWeight: 'bold',
-          fontFamily: 'Inter, sans-serif',
-          color: color,
-          offsetCenter: [0, '0'],
-          formatter: (value: number) => {
-            return `${value} `
-          }
-        },
-        data: [{ value: value }]
-      }
-    ]
-  })
+const chartContainer = ref<HTMLDivElement | null>(null)
+let chartInstance: ReturnType<typeof echarts.init> | null = null
+
+const toVisibleBarData = (data: number[]) => data.map((value) => (value > 0 ? value : null))
+const barValueLabel = {
+  show: true,
+  position: 'top',
+  color: CHART_COLORS.axis,
+  fontSize: 11,
+  fontWeight: 700,
+  formatter: ({ value }) => (Number(value) > 0 ? `${value}` : '')
 }
 
-const chartContainer = ref(null)
-let chartInstance = null
-
 // 初始化部门统计图表
 const initChart = () => {
   if (!chartContainer.value) return
 
   // 准备数据
-  const deptNames = deptDataList.value.map(item => item.name)
-  const totalCounts = deptDataList.value.map(item => item.totalCount)
-  const filledCounts = deptDataList.value.map(item => item.filledCount)
-  const unfilledCounts = deptDataList.value.map(item => item.unfilledCount)
-  const fillingCounts = deptDataList.value.map(item => item.fillingCount)
+  const deptNames = deptDataList.value.map((item) => item.name)
+  const totalCounts = deptDataList.value.map((item) => item.totalCount)
+  const filledCounts = deptDataList.value.map((item) => item.filledCount)
+  const unfilledCounts = deptDataList.value.map((item) => item.unfilledCount)
+  const fillingCounts = deptDataList.value.map((item) => item.fillingCount)
 
   // ECharts配置
   const option = {
@@ -520,9 +522,20 @@ const initChart = () => {
       axisPointer: {
         type: 'shadow'
       },
+      backgroundColor: 'rgba(255,255,255,0.96)',
+      borderColor: 'rgba(255,255,255,0.95)',
+      borderWidth: 1,
+      textStyle: {
+        color: '#3c3c3c'
+      },
+      extraCssText:
+        'border-radius: 8px; box-shadow: 0 8px 20px rgba(31, 91, 184, 0.16); backdrop-filter: blur(12px);',
       formatter: (params) => {
-        let result = `<div class="font-bold">${params[0].axisValue}</div>`
-        params.forEach(param => {
+        const visibleParams = params.filter((param) => Number(param.value) > 0)
+        if (!visibleParams.length) return ''
+
+        let result = `<div class="font-bold">${visibleParams[0].axisValue}</div>`
+        visibleParams.forEach((param) => {
           result += `<div>${param.marker} ${param.seriesName}: ${param.value}</div>`
         })
         return result
@@ -530,247 +543,486 @@ const initChart = () => {
     },
     legend: {
       data: ['总数', '已填写', '未填写', '填写中'],
-      top: 25
+      top: 8,
+      textStyle: {
+        color: CHART_COLORS.axis,
+        fontSize: 12
+      }
     },
     grid: {
-      left: '3%',
-      right: '4%',
-      bottom: '3%',
+      left: 24,
+      right: 24,
+      top: 48,
+      bottom: 16,
       containLabel: true
     },
     xAxis: {
       type: 'category',
       data: deptNames,
       axisLabel: {
-        rotate: 45,
-        margin: 15,
-        fontSize: 10
+        rotate: 0,
+        interval: 0,
+        hideOverlap: true,
+        overflow: 'truncate',
+        width: 120,
+        margin: 12,
+        color: CHART_COLORS.axis,
+        fontSize: 11
+      },
+      axisLine: {
+        lineStyle: {
+          color: CHART_COLORS.split
+        }
+      },
+      axisTick: {
+        show: false
       }
     },
     yAxis: {
       type: 'value',
       axisLabel: {
+        color: CHART_COLORS.axis,
         formatter: (value) => Math.floor(value).toString()
+      },
+      splitLine: {
+        lineStyle: {
+          color: CHART_COLORS.split
+        }
+      },
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
       }
     },
     series: [
       {
         name: '总数',
         type: 'bar',
-        barGap: 0,
+        barGap: '8%',
+        barCategoryGap: '28%',
+        barWidth: 72,
+        barMinHeight: 8,
+        label: barValueLabel,
         itemStyle: {
+          borderRadius: [8, 8, 0, 0],
           color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: '#188df0' },
-            { offset: 1, color: '#188df0' }
+            { offset: 0, color: CHART_COLORS.blueLight },
+            { offset: 1, color: CHART_COLORS.blue }
           ])
         },
         emphasis: {
           focus: 'series'
         },
-        data: totalCounts
+        data: toVisibleBarData(totalCounts)
       },
       {
         name: '已填写',
         type: 'bar',
+        barWidth: 72,
+        barMinHeight: 8,
+        label: barValueLabel,
         itemStyle: {
+          borderRadius: [8, 8, 0, 0],
           color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: '#d3a137' },
-            { offset: 1, color: '#d3a137' }
+            { offset: 0, color: '#a8e0c8' },
+            { offset: 1, color: CHART_COLORS.green }
           ])
         },
         emphasis: {
           focus: 'series'
         },
-        data: filledCounts
+        data: toVisibleBarData(filledCounts)
       },
       {
         name: '未填写',
         type: 'bar',
+        barWidth: 72,
+        barMinHeight: 8,
+        label: barValueLabel,
         itemStyle: {
+          borderRadius: [8, 8, 0, 0],
           color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'green' },
-            { offset: 1, color: 'green' }
+            { offset: 0, color: '#ffd8a8' },
+            { offset: 1, color: CHART_COLORS.orange }
           ])
         },
         emphasis: {
           focus: 'series'
         },
-        data: unfilledCounts
+        data: toVisibleBarData(unfilledCounts)
       },
       {
         name: '填写中',
         type: 'bar',
+        barWidth: 72,
+        barMinHeight: 8,
+        label: barValueLabel,
         itemStyle: {
+          borderRadius: [8, 8, 0, 0],
           color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'red' },
-            { offset: 1, color: 'red' }
+            { offset: 0, color: '#fdd5d5' },
+            { offset: 1, color: CHART_COLORS.red }
           ])
         },
         emphasis: {
           focus: 'series'
         },
-        data: fillingCounts
+        data: toVisibleBarData(fillingCounts)
       }
     ]
   }
 
   // 初始化图表
+  chartInstance?.dispose()
   chartInstance = echarts.init(chartContainer.value)
   chartInstance.setOption(option)
 
-  // 窗口缩放监听
-  window.addEventListener('resize', handleResize)
   handleResize()
 }
 
 // 自适应调整
 const handleResize = () => {
-  chartInstance?.resize()
-}
-const router = useRouter()
-const route = useRoute()
-// 监听路由变化
-const removeBeforeEach = router.beforeEach((to, from, next) => {
-  // 如果是从FillOrderInfo1页面返回
-  if (from.name === 'FillOrderInfo1' && to.name === route.name) {
-    // 恢复查询参数
-    Object.assign(queryParams, initialQueryParams)
-    // 阻止刷新,使用replace而不是push
-    next({ ...to, replace: true })
-  } else {
-    next()
+  const instance = chartInstance
+  if (instance) {
+    instance.resize()
   }
-})
+}
 
 /** 初始化 */
 onMounted(async () => {
-  queryParams.deptId = useUserStore().getUser.deptId;
-
-  // 计算近一周时间
-  const end = new Date();
-  const start = new Date();
-  start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000);
-
-  // 格式化日期为后端需要的格式
-  const formatDate = (date) => {
-    const year = date.getFullYear();
-    const month = String(date.getMonth() + 1).padStart(2, '0');
-    const day = String(date.getDate()).padStart(2, '0');
-    const hours = String(date.getHours()).padStart(2, '0');
-    const minutes = String(date.getMinutes()).padStart(2, '0');
-    const seconds = String(date.getSeconds()).padStart(2, '0');
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
-  };
-
-  queryParams.createTime = [formatDate(start), formatDate(end)];
+  queryParams.deptId = useUserStore().getUser.deptId ?? null
+
+  Object.assign(initialQueryParams, queryParams)
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  window.addEventListener('resize', updateScale)
 
   getStats()
-  getDevStats();
+  getDevStats()
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
-
 })
 
+onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  cancelAnimationFrame(resizeRaf)
+  chartInstance?.dispose()
+  chartInstance = null
+})
 </script>
 
 <style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.statistics-scale-wrapper {
+  overflow: hidden;
+}
+
 .statistics-page {
-  padding: 12px;
-  background: linear-gradient(180deg, #f5f8ff 0%, #f8fafc 100%);
+  display: flex;
+  overflow: hidden;
+  color: #24364f;
+  flex-direction: column;
+}
+
+.statistics-content {
+  position: relative;
+  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;
 }
 
 .section-row {
-  margin-bottom: 12px;
+  margin-bottom: calc(14px * var(--kb-scale));
+  flex-shrink: 0;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.trend-row {
+  min-height: 0;
+  flex: 1;
+
+  :deep(.el-col) {
+    min-height: 0;
+  }
 }
 
 .chart-card {
-  border: 1px solid #e8edf5;
-  border-radius: 12px;
-  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
+  overflow: hidden;
+  background: linear-gradient(180deg, rgb(235 243 255 / 76%) 0%, rgb(221 233 251 / 58%) 100%);
+  border: 1px solid rgb(255 255 255 / 58%);
+  border-radius: calc(20px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 72%),
+    0 18px 36px rgb(46 90 164 / 12%);
+  backdrop-filter: blur(12px);
+}
+
+.trend-card {
+  height: 100%;
+
+  :deep(.el-card__body) {
+    display: flex;
+    height: 100%;
+    min-height: 0;
+    flex-direction: column;
+  }
 }
 
 .filter-card {
   :deep(.el-card__body) {
-    padding: 14px 18px 2px;
+    padding-bottom: calc(6px * var(--kb-scale));
   }
 }
 
 .filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  padding: calc(12px * var(--kb-scale)) calc(16px * var(--kb-scale)) 0;
+  gap: 0 calc(12px * var(--kb-scale));
+
   :deep(.el-form-item) {
-    margin-bottom: 12px;
+    margin-bottom: calc(12px * var(--kb-scale));
+    align-items: center;
+  }
+
+  :deep(.el-form-item__label) {
+    width: calc(86px * var(--kb-scale)) !important;
+    height: calc(34px * var(--kb-scale));
+    font-size: calc(14px * var(--kb-scale));
+    font-weight: 600;
+    line-height: calc(34px * var(--kb-scale));
+    color: #03409b;
+    align-items: center;
+    white-space: nowrap;
+  }
+
+  :deep(.el-form-item__content) {
+    align-items: center;
+    line-height: calc(34px * var(--kb-scale));
   }
 
   :deep(.el-input__wrapper),
   :deep(.el-select__wrapper) {
-    border-radius: 10px;
+    min-height: calc(34px * var(--kb-scale));
+    padding: 0 calc(11px * var(--kb-scale));
+    background: linear-gradient(180deg, rgb(255 255 255 / 92%) 0%, rgb(242 247 255 / 82%) 100%);
+    border: 1px solid rgb(255 255 255 / 88%);
+    border-radius: calc(8px * var(--kb-scale));
+    box-shadow:
+      inset 0 1px 0 rgb(255 255 255 / 95%),
+      0 6px 12px rgb(152 181 230 / 12%);
+  }
+
+  :deep(.el-range-input),
+  :deep(.el-range-separator) {
+    font-size: calc(14px * var(--kb-scale));
+    color: #24364f;
   }
 }
 
+.filter-control {
+  width: calc(180px * var(--kb-scale));
+}
+
+.filter-control--date {
+  width: calc(220px * var(--kb-scale));
+}
+
 .filter-actions {
-  margin-left: 8px;
+  margin-left: calc(8px * var(--kb-scale));
 }
 
 .action-btn {
-  min-width: 88px;
-  border-radius: 10px;
+  height: calc(34px * var(--kb-scale));
+  min-width: calc(88px * var(--kb-scale));
+  padding: 0 calc(15px * var(--kb-scale));
+  font-size: calc(14px * var(--kb-scale));
+  font-weight: 600;
+  color: #f5f9ff;
+  background: linear-gradient(180deg, #83bcff 0%, #2f7ee9 58%, #1762d6 100%);
+  border: 1px solid rgb(255 255 255 / 55%);
+  border-radius: calc(8px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 58%),
+    0 4px 8px rgb(30 89 179 / 22%);
+
+  &:hover,
+  &:focus {
+    color: #fff;
+    border-color: rgb(255 255 255 / 72%);
+    filter: brightness(1.06);
+  }
 }
 
-.card-header {
-  display: flex;
-  align-items: center;
-  min-height: 24px;
+.metric-grid {
+  display: grid;
+  min-height: calc(214px * var(--kb-scale));
+  padding: calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale)) calc(16px * var(--kb-scale));
+  gap: calc(14px * var(--kb-scale));
 }
 
-.card-title {
-  font-size: 15px;
-  font-weight: 600;
-  color: #334155;
+.metric-grid--order {
+  grid-template-columns: repeat(5, minmax(0, 1fr));
 }
 
-.gauge-row {
-  height: 220px;
-  display: flex;
-  justify-content: space-between;
+.metric-grid--device {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
 }
 
-.gauge-item {
+.metric-card {
+  position: relative;
   display: flex;
-  flex-direction: column;
-  align-items: center;
-  border-radius: 10px;
-  padding: 4px 0;
+  min-width: 0;
+  min-height: calc(168px * var(--kb-scale));
+  padding: calc(18px * var(--kb-scale)) calc(18px * var(--kb-scale)) calc(16px * var(--kb-scale));
+  overflow: hidden;
+  text-align: left;
   cursor: pointer;
-  transition: all 0.2s ease;
+  background: linear-gradient(135deg, rgb(255 255 255 / 96%) 0%, rgb(239 246 255 / 88%) 100%),
+    radial-gradient(circle at 90% 12%, var(--metric-glow) 0%, rgb(255 255 255 / 0%) 38%);
+  border: 1px solid rgb(94 139 203 / 18%);
+  border-radius: calc(16px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 92%),
+    0 10px 22px rgb(46 90 164 / 9%);
+  transition:
+    transform 0.28s ease,
+    box-shadow 0.28s ease,
+    border-color 0.28s ease,
+    background 0.28s ease;
+  flex-direction: column;
+  justify-content: space-between;
 }
 
-.gauge-item:hover {
-  background: #f8fbff;
-  box-shadow: inset 0 0 0 1px #dbeafe;
+.metric-card::before {
+  position: absolute;
+  top: calc(14px * var(--kb-scale));
+  left: 0;
+  width: calc(4px * var(--kb-scale));
+  height: calc(100% - 28px * var(--kb-scale));
+  background: var(--metric-accent);
+  border-radius: 0 999px 999px 0;
+  content: '';
+  box-shadow: 0 0 12px var(--metric-glow);
 }
 
-.gauge-label {
-  margin-top: 6px;
-  font-size: 13px;
-  color: #64748b;
+.metric-card::after {
+  position: absolute;
+  right: calc(-28px * var(--kb-scale));
+  bottom: calc(-42px * var(--kb-scale));
+  width: calc(116px * var(--kb-scale));
+  height: calc(116px * var(--kb-scale));
+  pointer-events: none;
+  background: radial-gradient(circle, var(--metric-glow) 0%, rgb(255 255 255 / 0%) 72%);
+  content: '';
+  opacity: 0.7;
+  transition:
+    transform 0.28s ease,
+    opacity 0.28s ease;
 }
 
-.trend-chart {
-  height: 320px;
+.metric-card:hover {
+  background: linear-gradient(135deg, rgb(255 255 255 / 100%) 0%, rgb(232 242 255 / 92%) 100%),
+    radial-gradient(circle at 90% 12%, var(--metric-glow) 0%, rgb(255 255 255 / 0%) 42%);
+  border-color: rgb(58 103 180 / 28%);
+  transform: translateY(calc(-4px * var(--kb-scale)));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 92%),
+    0 16px 28px rgb(41 89 164 / 16%);
+}
+
+.metric-card:hover::after {
+  opacity: 1;
+  transform: scale(1.12);
+}
+
+.metric-card__label,
+.metric-card__hint,
+.metric-card__value,
+.metric-card__progress {
+  position: relative;
+  z-index: 1;
 }
 
-:deep(.el-card__header) {
-  border-bottom: 1px solid #eef2f7;
-  padding: 14px 18px;
+.metric-card__label {
+  font-size: calc(14px * var(--kb-scale));
+  font-weight: 700;
+  color: #18345a;
+}
+
+.metric-card__value {
+  margin-top: calc(14px * var(--kb-scale));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(38px * var(--kb-scale));
+  font-weight: normal;
+  line-height: 1;
+  color: var(--metric-accent);
+  text-shadow: 0 4px 10px rgb(255 255 255 / 76%);
+}
+
+.metric-card__count,
+:deep(.metric-card__count span) {
+  display: inline-flex;
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: inherit;
+  font-weight: normal;
+  line-height: 1;
+  letter-spacing: 1px;
+  color: inherit;
+}
+
+.metric-card__count {
+  transform: skewX(-6deg);
+  transform-origin: left center;
+}
+
+.metric-card__hint {
+  margin-top: calc(8px * var(--kb-scale));
+  font-size: calc(12px * var(--kb-scale));
+  font-weight: 700;
+  color: #536986;
+}
+
+.metric-card__progress {
+  display: block;
+  height: calc(6px * var(--kb-scale));
+  margin-top: calc(14px * var(--kb-scale));
+  overflow: hidden;
+  background: rgb(31 91 184 / 12%);
+  border-radius: 999px;
+}
+
+.metric-card__progress span {
+  display: block;
+  width: var(--metric-progress);
+  height: 100%;
+  background: var(--metric-accent);
+  border-radius: inherit;
+  box-shadow: 0 0 8px var(--metric-glow);
+}
+
+.trend-chart {
+  min-height: calc(260px * var(--kb-scale));
+  padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale));
+  flex: 1;
 }
 
 :deep(.el-card__body) {
-  padding: 14px 18px 16px;
+  padding: 0;
 }
 
-@media (max-width: 1366px) {
-  .gauge-row {
-    overflow-x: auto;
-    gap: 6px;
-    justify-content: flex-start;
-  }
+:deep(.el-card) {
+  --el-card-bg-color: transparent;
 }
 </style>

+ 186 - 67
src/views/pms/modeltemplate/TemplateForm.vue

@@ -1,54 +1,74 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-    >
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="设备分类" prop="deviceCategoryId">
-            <el-tree-select
-              v-model="formData.deviceCategoryId"
-              :data="deviceCategoryTree"
-              :props="defaultProps"
-              check-strictly
-              default-expand-all
-              value-key="deviceCategoryId"
-              placeholder="请选择设备分类"
-              @node-click="handleDeviceCategoryTreeNodeClick"
-              clearable
-              filterable
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="模板名称" prop="name">
-            <el-input v-model="formData.name" maxlength="32" placeholder="请输入模板名称" />
-          </el-form-item>
-        </el-col>
-<!--        <el-col :span="12">
-          <el-form-item label="模板编码" prop="code">
-            <el-input v-model="formData.code" maxlength="50" placeholder="请输入模板编码" />
-          </el-form-item>
-        </el-col>-->
-      </el-row>
-      <el-row>
-        <el-col :span="24">
-          <el-form-item label="备注">
-            <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-    </el-form>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="920px" class="model-template-dialog">
+    <template #title>
+      <div class="dialog-title">
+        <div class="dialog-title__icon">
+          <Icon icon="ep:set-up" :size="22" />
+        </div>
+        <div>
+          <div class="dialog-title__text">{{ dialogTitle }}</div>
+          <div class="dialog-title__desc">维护设备分类下的模型属性模板信息</div>
+        </div>
+      </div>
+    </template>
+
+    <div v-loading="formLoading" class="template-form-body">
+      <section class="template-form-card">
+        <div class="template-form-card__header">
+          <div class="template-form-card__title">
+            <span class="template-form-card__marker"></span>
+            <span>模板信息</span>
+          </div>
+        </div>
+
+        <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          size="default"
+          label-position="top"
+          class="template-form">
+          <el-row :gutter="20">
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="设备分类" prop="deviceCategoryId">
+                <el-tree-select
+                  v-model="formData.deviceCategoryId"
+                  :data="deviceCategoryTree"
+                  :props="defaultProps"
+                  check-strictly
+                  default-expand-all
+                  value-key="deviceCategoryId"
+                  placeholder="请选择设备分类"
+                  clearable
+                  filterable
+                  class="w-full"
+                  @node-click="handleDeviceCategoryTreeNodeClick" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="模板名称" prop="name">
+                <el-input v-model="formData.name" maxlength="32" placeholder="请输入模板名称" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24">
+              <el-form-item label="备注">
+                <el-input
+                  v-model="formData.remark"
+                  placeholder="请输入内容"
+                  type="textarea"
+                  :autosize="{ minRows: 3, maxRows: 5 }" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </section>
+    </div>
+
     <template #footer>
-      <div v-show="showButton">
-        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <div v-show="showButton" class="dialog-footer">
         <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       </div>
-
     </template>
   </Dialog>
 </template>
@@ -58,15 +78,14 @@ import { defaultProps, handleTree } from '@/utils/tree'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
 import * as DeviceTemplateApi from '@/api/pms/modeltemplate'
 import { FormRules } from 'element-plus'
-import { useTreeStore } from '@/store/modules/attrTemplateTreeStore';
-import {ModelAttrTemplateVO} from "@/api/pms/modeltemplate";
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
 
 defineOptions({ name: 'DeviceTemplateForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const treeStore = useTreeStore();
-const localDeviceCategoryId = ref(null);  // 通过store存储的设备分类id 由父组件传递过来
+const treeStore = useTreeStore()
+const localDeviceCategoryId = ref(null) // 通过store存储的设备分类id 由父组件传递过来
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -77,7 +96,7 @@ const formData = ref({
   code: '',
   id: undefined,
   remark: '',
-  status: CommonStatusEnum.ENABLE,
+  status: CommonStatusEnum.ENABLE
 })
 const formData1 = ref({
   name: '',
@@ -85,22 +104,21 @@ const formData1 = ref({
   code: '',
   id: undefined,
   remark: '',
-  status: CommonStatusEnum.ENABLE,
+  status: CommonStatusEnum.ENABLE
 })
 const formRules = reactive<FormRules>({
   name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
-  deviceCategoryId: [{ required: true, message: '所属设备分类不能为空', trigger: 'blur' }],
-
+  deviceCategoryId: [{ required: true, message: '所属设备分类不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const deviceCategoryTree = ref()  // 设备分类树
-const showButton = ref(true);
+const deviceCategoryTree = ref() // 设备分类树
+const showButton = ref(true)
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   // 获取store中的设备分类id
-  localDeviceCategoryId.value = treeStore.selectedId;
+  localDeviceCategoryId.value = treeStore.selectedId
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
@@ -158,18 +176,18 @@ const getDeviceCategoryTree = async () => {
 /** 处理 设备分类 树 被点击 */
 const handleDeviceCategoryTreeNodeClick = async (row: { [key: string]: any }) => {
   emit('node-click', row)
-  isRepated(row.id);
+  isRepated(row.id)
   localDeviceCategoryId.value = row.id
 }
 
-const isRepated = async(id)=>{
-  showButton.value=true;
-  formData1.value = await DeviceTemplateApi.getAttrTemplateByModelCategoryId(id);
-  if(formData1.value){
+const isRepated = async (id) => {
+  showButton.value = true
+  formData1.value = await DeviceTemplateApi.getAttrTemplateByModelCategoryId(id)
+  if (formData1.value) {
     message.error(t('form.repeatDevice'))
-    showButton.value=false;
-  }else{
-    showButton.value=true;
+    showButton.value = false
+  } else {
+    showButton.value = true
   }
 }
 
@@ -181,8 +199,109 @@ const resetForm = () => {
     code: '',
     id: undefined,
     remark: '',
-    status: CommonStatusEnum.ENABLE,
+    status: CommonStatusEnum.ENABLE
   }
   formRef.value?.resetFields()
 }
 </script>
+
+<style scoped>
+.dialog-title {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.dialog-title__icon {
+  display: flex;
+  width: 40px;
+  height: 40px;
+  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__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__desc {
+  margin-top: 3px;
+  font-size: 13px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.template-form-body {
+  padding: 4px 0;
+}
+
+.template-form-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 8px 24px rgb(31 45 61 / 5%);
+}
+
+.template-form-card__header {
+  display: flex;
+  min-height: 52px;
+  padding: 0 18px;
+  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;
+}
+
+.template-form-card__title {
+  display: flex;
+  gap: 10px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.template-form-card__marker {
+  width: 4px;
+  height: 17px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.template-form {
+  padding: 20px 22px 4px;
+}
+
+.template-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.template-form :deep(.el-form-item__label) {
+  margin-bottom: 6px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+
+@media (width <= 768px) {
+  .dialog-title__desc {
+    display: none;
+  }
+
+  .template-form {
+    padding: 16px 16px 0;
+  }
+}
+</style>

+ 268 - 126
src/views/pms/modeltemplate/detail/attrsModel/index.vue

@@ -1,180 +1,94 @@
 <!-- 设备分类属性列表 -->
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      ref="queryFormRef"
-      :inline="true"
-      :model="queryParams"
-      class="-mb-15px"
-      label-width="68px"
-    >
-      <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
-        <el-button
-          v-hasPermi="[`rq:iot-model-template-attrs:create`]"
-          plain
-          type="primary"
-          @click="openForm('create')"
-        >
-          <Icon class="mr-5px" icon="ep:plus" />
-          添加属性
-        </el-button>
-      </el-form-item>
-      <el-form-item label="所属模板:" style="width: 200px" label-width="100px">
-        <span>
-          {{modelName}}
-        </span>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-tabs>
-      <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-        <el-table-column align="center" label="属性名称" prop="name" />
-<!--        <el-table-column align="center" label="标识符" prop="code" />-->
-        <el-table-column align="center" label="数据类型" prop="type">
-          <template #default="{ row }">
-            {{ dataTypeOptionsLabel(row.selectOptions?.type) ?? '-' }}
-          </template>
-        </el-table-column>
-        <!--
-        <el-table-column align="left" label="数据定义" prop="code">
-          <template #default="{ row }">
-            <DataDefinition :data="row" />
-          </template>
-        </el-table-column>
-        -->
-        <el-table-column align="center" label="操作">
-          <template #default="scope">
-            <el-button
-              v-hasPermi="[`rq:iot-model-template-attrs:update`]"
-              link
-              type="primary"
-              @click="openForm('update', scope.row.id)"
-            >
-              编辑
-            </el-button>
-            <el-button
-              v-hasPermi="['rq:iot-model-template-attrs:delete']"
-              link
-              type="danger"
-              @click="handleDelete(scope.row.id,scope.row.deviceCategoryId)"
-            >
-              删除
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <!-- 分页 -->
-      <Pagination
-        v-model:limit="queryParams.pageSize"
-        v-model:page="queryParams.pageNo"
-        :total="total"
-        @pagination="getList"
-      />
-    </el-tabs>
-  </ContentWrap>
-  <!-- 表单弹窗:添加/修改 -->
-  <AttrTemplateModelForm ref="formRef" @success="getList" :deviceCategoryId="deviceCategoryId" />
-</template>
 <script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { DeviceAttrModelApi, DeviceAttrModelData } from '@/api/pms/modelattrtemplate'
 import AttrTemplateModelForm from './AttrTemplateModelForm.vue'
 import { getDataTypeOptionsLabel } from './config'
-import { DataDefinition } from '../components'
-import {useTagsViewStore} from "@/store/modules/tagsView";
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+defineOptions({ name: 'ModelAttrTemplate' })
+
+interface QueryParams extends PageParam {
+  type?: string
+  deviceCategoryId: string | number
+}
 
-const { delView } = useTagsViewStore() // 视图操作
+const { delView } = useTagsViewStore()
 const route = useRoute()
 const { currentRoute } = useRouter()
+const { t } = useI18n()
+const message = useMessage()
+const { ZmTable, ZmTableColumn } = useTableComponents<DeviceAttrModelData>()
 
-defineOptions({ name: 'ModelAttrTemplate' })
+const routeParams = computed(() => String(route.params.id || '').split(','))
+const deviceCategoryId = computed(() => routeParams.value[0])
+const categoryName = computed(() => routeParams.value[1] || '-')
+const modelName = computed(() => routeParams.value[2] || '-')
 
-const deviceCategoryId = route.params.id.split(",")[0] // 设备分类id
-const categoryName = route.params.id.split(",")[1];//设备分类名称
-const modelName = route.params.id.split(",")[2];//模板名称
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const loading = ref(true) // 列表的加载中
-const list = ref<DeviceAttrModelData[]>([]) // 列表的数据
-const list1 = ref<DeviceAttrModelData[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const loading = ref(true)
+const list = ref<DeviceAttrModelData[]>([])
+const list1 = ref<DeviceAttrModelData[]>([])
+const total = ref(0)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   type: undefined,
   deviceCategoryId: -1
 })
 
-const queryFormRef = ref() // 搜索的表单
-const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型
+const queryFormRef = ref()
+const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value))
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    queryParams.deviceCategoryId = deviceCategoryId
+    queryParams.deviceCategoryId = deviceCategoryId.value
     const data = await DeviceAttrModelApi.getDeviceAttrModelPage(queryParams)
     list.value = data.list
-    list.value.forEach(function (item,index){
-      if(item.isSum!=1){
-        list1.value.push(item)
-      }
-    })
+    list1.value = data.list.filter((item) => item.isSum !== 1)
     total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   queryParams.type = undefined
   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,name?:string,list?:[]) => {
-  name = categoryName;
-  list = list1;
-    formRef.value.open(type,id,name,list)
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id, categoryName.value, list1)
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
-
-    // 删除的二次确认
     await message.delConfirm()
-    // 发起删除
     await DeviceAttrModelApi.deleteDeviceAttrModel(id)
     message.success(t('common.delSuccess'))
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 初始化 **/
 onMounted(() => {
-  if (!deviceCategoryId) {
+  if (!deviceCategoryId.value) {
     message.warning('参数错误,设备属性模板不能为空!')
     delView(unref(currentRoute))
     return
@@ -182,3 +96,231 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="model-attr-detail h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <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">
+        <div class="detail-grid">
+          <div class="detail-item">
+            <div class="detail-item__label">所属模板</div>
+            <div class="detail-item__value">{{ modelName }}</div>
+          </div>
+          <div class="detail-item">
+            <div class="detail-item__label">设备分类</div>
+            <div class="detail-item__value">{{ categoryName }}</div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="info-card attr-card">
+      <div class="info-card__header">
+        <div class="info-card__title">
+          <span class="info-card__marker"></span>
+          <span>属性列表</span>
+        </div>
+        <span class="card-total">共 {{ total }} 条</span>
+      </div>
+
+      <div class="info-card__body attr-card__body">
+        <el-form ref="queryFormRef" :model="queryParams" size="default" class="table-toolbar">
+          <div class="flex items-center gap-2">
+            <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
+              v-hasPermi="[`rq:iot-model-template-attrs:create`]"
+              plain
+              type="primary"
+              @click="openForm('create')">
+              <Icon icon="ep:plus" class="mr-5px" />添加属性
+            </el-button>
+          </div>
+        </el-form>
+
+        <div class="flex-1 relative min-h-0">
+          <el-auto-resizer class="absolute">
+            <template #default="{ width, height }">
+              <ZmTable
+                :data="list"
+                :loading="loading"
+                :width="width"
+                :height="height"
+                :max-height="height"
+                show-border>
+                <ZmTableColumn
+                  type="index"
+                  label="序号"
+                  width="70"
+                  fixed="left"
+                  hide-in-column-settings />
+                <ZmTableColumn prop="name" label="属性名称" fixed="left" min-width="180" />
+                <ZmTableColumn prop="type" label="数据类型" min-width="150">
+                  <template #default="{ row }">
+                    {{ dataTypeOptionsLabel(row.selectOptions?.type) ?? '-' }}
+                  </template>
+                </ZmTableColumn>
+                <ZmTableColumn label="操作" width="180" fixed="right" action>
+                  <template #default="{ row }">
+                    <div class="flex items-center justify-center gap-2">
+                      <el-button
+                        v-hasPermi="[`rq:iot-model-template-attrs:update`]"
+                        link
+                        type="primary"
+                        @click="openForm('update', row.id)">
+                        <Icon icon="ep:edit" class="mr-3px" />编辑
+                      </el-button>
+                      <el-button
+                        v-hasPermi="['rq:iot-model-template-attrs:delete']"
+                        link
+                        type="danger"
+                        @click="handleDelete(row.id)">
+                        <Icon icon="ep:delete" class="mr-3px" />删除
+                      </el-button>
+                    </div>
+                  </template>
+                </ZmTableColumn>
+              </ZmTable>
+            </template>
+          </el-auto-resizer>
+        </div>
+
+        <div class="h-8 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>
+    </section>
+
+    <AttrTemplateModelForm ref="formRef" :deviceCategoryId="deviceCategoryId" @success="getList" />
+  </div>
+</template>
+
+<style scoped>
+.model-attr-detail {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.info-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 10px 28px rgb(31 45 61 / 6%);
+}
+
+.info-card__header {
+  display: flex;
+  min-height: 58px;
+  padding: 0 20px;
+  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;
+  justify-content: space-between;
+}
+
+.info-card__title {
+  display: flex;
+  gap: 10px;
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.info-card__marker {
+  width: 4px;
+  height: 18px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.info-card__body {
+  padding: 20px;
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.detail-item {
+  min-width: 0;
+  padding: 14px 16px;
+  background: var(--el-fill-color-lighter);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.detail-item__label {
+  margin-bottom: 6px;
+  font-size: 13px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.detail-item__value {
+  overflow: hidden;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.attr-card {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.attr-card__body {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  gap: 12px;
+  min-height: 0;
+}
+
+.card-total {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+}
+
+.table-toolbar :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width <= 768px) {
+  .detail-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+}
+</style>

+ 261 - 224
src/views/pms/modeltemplate/index.vue

@@ -1,202 +1,41 @@
-<template>
-  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
-  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
-  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
-
-  <el-row :gutter="20">
-    <!-- 左侧 设备分类 树 -->
-    <el-col :span="4" :xs="24">
-      <ContentWrap class="h-1/1">
-        <ModelCategoryTree @node-click="handleModelCategoryTreeNodeClick" />
-      </ContentWrap>
-    </el-col>
-    <el-col :span="20" :xs="24">
-      <!-- 搜索 -->
-      <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('modelTemplate.name')" prop="name" style="margin-left: 45px">
-            <el-input
-              v-model="queryParams.name"
-              :placeholder="t('modelTemplate.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <!--          <el-form-item label="模板编码" prop="code">
-            <el-input
-              v-model="queryParams.code"
-              placeholder="请输入模板编码"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>-->
-          <el-form-item :label="t('modelTemplate.status')" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              :placeholder="t('modelTemplate.status')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('modelTemplate.createTime')" prop="createTime">
-            <el-date-picker
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="datetimerange"
-              :start-placeholder="t('operationFill.start')"
-              :end-placeholder="t('operationFill.end')"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" />{{ t('operationFill.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" />{{ t('operationFill.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-model-template:create']"
-            >
-              <Icon icon="ep:plus" />{{ t('operationFill.add') }}
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </div>
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list">
-          <!--          <el-table-column
-            label="模板编码"
-            align="center"
-            prop="code"
-            :show-overflow-tooltip="true"
-          />-->
-          <zm-table-column
-            :label="t('modelTemplate.name')"
-            align="center"
-            prop="name"
-            :show-overflow-tooltip="true"
-          />
-          <zm-table-column
-            :label="t('modelTemplate.ec')"
-            align="center"
-            key="deviceCategoryName"
-            prop="deviceCategoryName"
-            :show-overflow-tooltip="true"
-          />
-          <zm-table-column :label="t('modelTemplate.status')" key="status">
-            <template #default="scope">
-              <el-switch
-                v-model="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="handleStatusChange(scope.row)"
-                :disabled="!checkPermi(['rq:iot-model-template:update'])"
-              />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('modelTemplate.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-          />
-          <zm-table-column :label="t('operationFill.operation')" align="center" width="220" action>
-            <template #default="scope">
-              <div class="flex items-center justify-center">
-                <el-button
-                  type="primary"
-                  link
-                  @click="
-                    openDetail(
-                      scope.row.deviceCategoryId +
-                        ',' +
-                        scope.row.deviceCategoryName +
-                        ',' +
-                        scope.row.name
-                    )
-                  "
-                >
-                  <Icon icon="ep:edit" />{{ t('operationFill.view') }}
-                </el-button>
-                <el-button
-                  type="primary"
-                  link
-                  @click="openForm('update', scope.row.id)"
-                  v-hasPermi="['rq:iot-model-template:update']"
-                >
-                  <Icon icon="ep:edit" />{{ t('modelTemplate.update') }}
-                </el-button>
-
-                <el-button
-                  type="danger"
-                  link
-                  @click="handleDelete(scope.row.id, scope.row.deviceCategoryId)"
-                  v-hasPermi="['rq:iot-model-template:delete']"
-                >
-                  <Icon icon="ep:delete" />{{ t('modelTemplate.delete') }}
-                </el-button>
-              </div>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-
-  <!-- 添加或修改 属性模板 对话框 -->
-  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
-</template>
 <script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { checkPermi } from '@/utils/permission'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, rangeShortcuts } from '@/utils/formatTime'
 import { CommonStatusEnum } from '@/utils/constants'
 import * as ModelTemplateApi from '@/api/pms/modeltemplate'
+import DeviceCategoryTree from '@/components/DeviceCategoryTree/index.vue'
 import TemplateForm from './TemplateForm.vue'
-import ModelCategoryTree from './ModelCategoryTree.vue'
 import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
 
 defineOptions({ name: 'ModelAttrsTemplate' })
 
-const treeStore = useTreeStore()
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type ModelTemplateRow = Omit<ModelTemplateApi.ModelAttrTemplateVO, 'status'> & {
+  status?: number
+  deviceCategoryName?: string
+  createTime?: string
+}
 
+interface QueryParams extends PageParam {
+  name?: string
+  code?: string
+  status?: number
+  deviceCategoryId?: number
+  deviceCategoryName?: string
+  createTime?: string[]
+}
+
+const treeStore = useTreeStore()
+const message = useMessage()
+const { t } = useI18n()
 const { push } = useRouter()
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数
-const queryParams = reactive({
+const { ZmTable, ZmTableColumn } = useTableComponents<ModelTemplateRow>()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<ModelTemplateRow[]>([])
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   name: undefined,
@@ -206,81 +45,106 @@ const queryParams = reactive({
   deviceCategoryName: undefined,
   createTime: []
 })
-const queryFormRef = ref() // 搜索的表单
+const queryFormRef = ref()
 
-// 从 Store 中获取左侧 设备分类树 选中的 节点ID
 const selectedId = computed(() => treeStore.selectedId)
 
-const isInitialized = ref(false)
-/** 查询 设备属性模板 列表 */
+const normalizeStatus = (status: unknown) => {
+  const value = Number(status)
+  if (value === CommonStatusEnum.ENABLE || value === CommonStatusEnum.DISABLE) {
+    return value
+  }
+  return undefined
+}
+
+const isValidStatus = (status: unknown) => normalizeStatus(status) !== undefined
+
+const getStatusText = (status: unknown) => {
+  const value = normalizeStatus(status)
+  if (value === CommonStatusEnum.ENABLE) return '启用'
+  if (value === CommonStatusEnum.DISABLE) return '停用'
+  return '未知'
+}
+
 const getList = async () => {
   loading.value = true
   try {
     const data = await ModelTemplateApi.getModelTemplatePage(queryParams)
-    list.value = data.list
+    list.value = data.list.map((item: ModelTemplateRow) => ({
+      ...item,
+      status: normalizeStatus(item.status)
+    }))
     total.value = data.total
-    // 数据加载完成后,标记为已初始化
-    setTimeout(() => {
-      isInitialized.value = true
-    }, 100)
   } finally {
     loading.value = false
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 处理 设备分类树 被点击 */
-const handleModelCategoryTreeNodeClick = async (row) => {
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeviceCategoryTreeNodeClick = async (row: Tree) => {
+  treeStore.setSelectedId(row.id)
   queryParams.deviceCategoryId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 
-/** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
-/** 打开详情 */
-const openDetail = (id: string) => {
-  push({ name: 'ModelAttrTemplate', params: { id } })
+const openDetail = (row: ModelTemplateRow) => {
+  push({
+    name: 'ModelAttrTemplate',
+    params: {
+      id: `${row.deviceCategoryId},${row.deviceCategoryName},${row.name}`
+    }
+  })
 }
 
-/** 修改 设备属性模板 状态 */
-const handleStatusChange = async (row: ModelTemplateApi.ModelAttrTemplateVO) => {
-  // 如果还未初始化完成,直接返回
-  if (!isInitialized.value) {
+const handleStatusChange = async (row: ModelTemplateRow, nextStatus: string | number | boolean) => {
+  const status = normalizeStatus(nextStatus)
+  const oldStatus = normalizeStatus(row.status)
+  if (
+    !row.id ||
+    !row.name ||
+    status === undefined ||
+    oldStatus === undefined ||
+    status === oldStatus
+  ) {
     return
   }
 
   try {
-    // 修改状态的二次确认
-    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '""' + row.name + '"属性模板吗?')
-    // 发起修改状态
-    await ModelTemplateApi.updateModelTemplateStatus(row.id, row.status)
-    // 刷新列表
+    await message.confirm(`确认要${getStatusText(status)}"${row.name}"属性模板吗?`)
+    row.status = status
+    await ModelTemplateApi.updateModelTemplateStatus(row.id, status)
     await getList()
   } catch {
-    // 取消后,进行恢复按钮
-    row.status =
-      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+    row.status = oldStatus
   }
 }
 
-/** 操作分发 */
-const handleCommand = (command: string, row: ModelTemplateApi.ModelAttrTemplateVO) => {
+const handleCommand = (command: string, row: ModelTemplateRow) => {
   switch (command) {
     case 'handleDelete':
       handleDelete(row.id, row.deviceCategoryId)
@@ -290,27 +154,200 @@ const handleCommand = (command: string, row: ModelTemplateApi.ModelAttrTemplateV
   }
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number | undefined, deviceId: number) => {
   try {
     const data = await ModelTemplateApi.getIsRelated(deviceId)
     if (data.length === 0) {
-      // 删除的二次确认
       await message.delConfirm()
-      // 发起删除
       await ModelTemplateApi.deleteModelTemplate(id)
       message.success(t('common.delSuccess'))
     } else {
       message.error(t('form.relatedModel'))
     }
 
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 初始化 */
 onMounted(() => {
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeviceCategoryTree
+      v-model="queryParams.deviceCategoryId"
+      :show-title="false"
+      class="row-span-2"
+      @node-click="handleDeviceCategoryTreeNodeClick" />
+
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-3 gap-6 flex items-center justify-between flex-wrap min-w-0">
+      <div class="flex items-center gap-6 flex-wrap">
+        <el-form-item :label="t('modelTemplate.name')" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            :placeholder="t('modelTemplate.nameHolder')"
+            clearable
+            class="!w-200px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('modelTemplate.status')" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            :placeholder="t('modelTemplate.status')"
+            clearable
+            class="!w-180px">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('modelTemplate.createTime')" prop="createTime">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="datetimerange"
+            :start-placeholder="t('operationFill.start')"
+            :end-placeholder="t('operationFill.end')"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-320px" />
+        </el-form-item>
+      </div>
+
+      <el-form-item>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ 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-model-template:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn
+                prop="name"
+                :label="t('modelTemplate.name')"
+                fixed="left"
+                min-width="180"
+                show-overflow-tooltip />
+              <ZmTableColumn
+                prop="deviceCategoryName"
+                :label="t('modelTemplate.ec')"
+                min-width="160"
+                show-overflow-tooltip />
+              <ZmTableColumn
+                prop="status"
+                :label="t('modelTemplate.status')"
+                :real-value="(row) => getStatusText(row.status)"
+                width="90">
+                <template #default="{ row }">
+                  <el-switch
+                    v-if="isValidStatus(row.status)"
+                    :model-value="row.status"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="!checkPermi(['rq:iot-model-template:update'])"
+                    @change="(status) => handleStatusChange(row, status)" />
+                  <el-tag v-else type="info">未知</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('modelTemplate.createTime')"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn :label="t('operationFill.operation')" width="220" fixed="right" action>
+                <template #default="{ row }">
+                  <div class="flex items-center justify-center gap-2">
+                    <el-button type="primary" link @click="openDetail(row)">
+                      <Icon icon="ep:view" class="mr-3px" />{{ t('operationFill.view') }}
+                    </el-button>
+                    <el-button
+                      type="primary"
+                      link
+                      @click="openForm('update', row.id)"
+                      v-hasPermi="['rq:iot-model-template:update']">
+                      <Icon icon="ep:edit" class="mr-3px" />{{ t('modelTemplate.update') }}
+                    </el-button>
+                    <el-dropdown
+                      @command="(command) => handleCommand(command, row)"
+                      v-hasPermi="['rq:iot-model-template:delete']">
+                      <el-button type="primary" link>
+                        <Icon icon="ep:more-filled" class="mr-3px" />更多
+                      </el-button>
+                      <template #dropdown>
+                        <el-dropdown-menu>
+                          <el-dropdown-item
+                            command="handleDelete"
+                            v-if="checkPermi(['rq:iot-model-template:delete'])">
+                            <Icon icon="ep:delete" class="mr-5px" />{{ t('modelTemplate.delete') }}
+                          </el-dropdown-item>
+                        </el-dropdown-menu>
+                      </template>
+                    </el-dropdown>
+                  </div>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
+  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>