瀏覽代碼

Merge branch 'master' of http://1.94.244.160:3000/shuzhihua/pms-iot-vue

Zimo 2 天之前
父節點
當前提交
9b4e43e393

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

@@ -46,6 +46,10 @@ export const IotMeasureCertApi = {
   // 导出计量器具-证书管理 Excel
   exportIotMeasureCert: async (params) => {
     return await request.download({ url: `/rq/iot-measure-cert/export-excel`, params })
+  },
+  // 统计
+  getIotMeasureCertStatistics: async (id) => {
+    return await request.get({ url: `/rq/iot-measure-cert/stat?deptId=${id}` })
   }
 }
 
@@ -70,6 +74,10 @@ export const IotInstrumentApi = {
   // 导出计量器具台账 Excel
   exportInstrument: async (params) => {
     return await request.download({ url: `/rq/iot-measure-book/export-excel`, params })
+  },
+  //统计
+  getInstrumentStatistics: async (id) => {
+    return await request.get({ url: `/rq/iot-measure-book/stat?deptId=${id}` })
   }
 }
 
@@ -285,6 +293,10 @@ export const IotHiddenApi = {
   // 整改
   rectifyHidden: async (data) => {
     return await request.put({ url: `/rq/iot-hazard/rectify`, data })
+  },
+  // 统计
+  getHiddenStatistics: async (id) => {
+    return await request.get({ url: `/rq/iot-hazard-type?deptId=${id}` })
   }
 }
 
@@ -407,3 +419,9 @@ export const QHSEPtwApi = {
     return await request.download({ url: `/rq/qhse-ptw/export-excel`, params })
   }
 }
+
+export const kanbanApi = {
+  getKanban: async (params) => {
+    return await request.get({ url: `/rq/qhse-kanban/get`, params })
+  }
+}

+ 35 - 50
src/views/iot/alert/config/index.vue

@@ -1,35 +1,28 @@
 <template>
-  <ContentWrap>
+  <div
+    style="border: none; overflow: hidden; background-color: #fff"
+    class="rounded-md px-4 pt-4 mb-4">
     <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
+    <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
       <el-form-item label="配置名称" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入配置名称"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="配置状态" prop="status">
         <el-select
           v-model="queryParams.status"
           placeholder="请选择配置状态"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -40,8 +33,7 @@
           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>
@@ -50,86 +42,79 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['iot:alert-config:create']"
-        >
+          v-hasPermi="['iot:alert-config:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
       </el-form-item>
     </el-form>
-  </ContentWrap>
+  </div>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table
+  <ContentWrap style="border: none">
+    <zm-table
       row-key="id"
-      v-loading="loading"
+      :loading="loading"
       :data="list"
       :stripe="true"
-      :show-overflow-tooltip="true"
-    >
-      <el-table-column label="配置编号" align="center" prop="id" />
-      <el-table-column label="配置名称" align="center" prop="name" />
-      <el-table-column label="配置描述" align="center" prop="description" />
-      <el-table-column label="告警级别" align="center" prop="level">
+      :show-overflow-tooltip="true">
+      <zm-table-column label="配置编号" align="center" prop="id" />
+      <zm-table-column label="配置名称" align="center" prop="name" />
+      <zm-table-column label="配置描述" align="center" prop="description" />
+      <zm-table-column label="告警级别" align="center" prop="level">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_ALERT_LEVEL" :value="scope.row.level" />
         </template>
-      </el-table-column>
-      <el-table-column label="配置状态" align="center" prop="status">
+      </zm-table-column>
+      <zm-table-column label="配置状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
-      </el-table-column>
-      <el-table-column label="关联场景联动规则" align="center" prop="sceneRuleIds" min-width="100">
+      </zm-table-column>
+      <zm-table-column label="关联场景联动规则" align="center" prop="sceneRuleIds" min-width="100">
         <template #default="scope"> {{ scope.row.sceneRuleIds?.length || 0 }} 条 </template>
-      </el-table-column>
-      <el-table-column label="接收人" align="center" prop="receiveUserNames" />
-      <el-table-column label="接收类型" align="center" prop="receiveTypes">
+      </zm-table-column>
+      <zm-table-column label="接收人" align="center" prop="receiveUserNames" />
+      <zm-table-column label="接收类型" align="center" prop="receiveTypes">
         <template #default="scope">
           <dict-tag
             v-for="(receiveType, index) in scope.row.receiveTypes"
             :key="index"
             :type="DICT_TYPE.IOT_ALERT_RECEIVE_TYPE"
             :value="receiveType"
-            class="mr-1"
-          />
+            class="mr-1" />
         </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="120px">
+        width="180px" />
+      <zm-table-column label="操作" align="center" min-width="120px" action>
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:alert-config:update']"
-          >
+            v-hasPermi="['iot:alert-config:update']">
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:alert-config:delete']"
-          >
+            v-hasPermi="['iot:alert-config:delete']">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 41 - 61
src/views/iot/alert/record/index.vue

@@ -1,27 +1,21 @@
 <template>
-  <ContentWrap>
+  <div
+    style="border: none; overflow: hidden; background-color: #fff"
+    class="rounded-md px-4 pt-4 mb-4">
     <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
+    <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
       <el-form-item label="告警配置" prop="configId">
         <el-select
           v-model="queryParams.configId"
           placeholder="请选择告警配置"
           clearable
           filterable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="config in alertConfigList"
             :key="config.id"
             :label="config.name"
-            :value="config.id"
-          />
+            :value="config.id" />
         </el-select>
       </el-form-item>
       <el-form-item label="告警级别" prop="configLevel">
@@ -29,14 +23,12 @@
           v-model="queryParams.configLevel"
           placeholder="请选择告警级别"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_LEVEL)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="产品" prop="productId">
@@ -46,14 +38,12 @@
           clearable
           filterable
           @change="handleProductChange"
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="product in productList"
             :key="product.id"
             :label="product.name"
-            :value="product.id"
-          />
+            :value="product.id" />
         </el-select>
       </el-form-item>
       <el-form-item label="设备" prop="deviceId">
@@ -62,14 +52,12 @@
           placeholder="请选择设备"
           clearable
           filterable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="device in filteredDeviceList"
             :key="device.id"
             :label="device.deviceName"
-            :value="device.id"
-          />
+            :value="device.id" />
         </el-select>
       </el-form-item>
       <el-form-item label="是否处理" prop="processStatus">
@@ -77,14 +65,12 @@
           v-model="queryParams.processStatus"
           placeholder="请选择是否处理"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
             :key="String(dict.value)"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -95,50 +81,47 @@
           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-form-item>
     </el-form>
-  </ContentWrap>
+  </div>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table
+  <ContentWrap style="border: none">
+    <zm-table
       row-key="id"
-      v-loading="loading"
+      :loading="loading"
       :data="list"
       :stripe="true"
-      :show-overflow-tooltip="true"
-    >
-      <el-table-column label="记录编号" align="center" prop="id" />
-      <el-table-column label="告警名称" align="center" prop="configName" />
-      <el-table-column label="告警级别" align="center" prop="configLevel">
+      :show-overflow-tooltip="true">
+      <zm-table-column label="记录编号" align="center" prop="id" />
+      <zm-table-column label="告警名称" align="center" prop="configName" />
+      <zm-table-column label="告警级别" align="center" prop="configLevel">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_ALERT_LEVEL" :value="scope.row.configLevel" />
         </template>
-      </el-table-column>
-      <el-table-column label="产品名称" align="center" prop="productId">
+      </zm-table-column>
+      <zm-table-column label="产品名称" align="center" prop="productId">
         <template #default="scope">
           {{ getProductName(scope.row.productId) }}
         </template>
-      </el-table-column>
-      <el-table-column label="设备名称" align="center" prop="deviceId">
+      </zm-table-column>
+      <zm-table-column label="设备名称" align="center" prop="deviceId">
         <template #default="scope">
           {{ getDeviceName(scope.row.deviceId) }}
         </template>
-      </el-table-column>
-      <el-table-column label="触发的设备消息" align="center" prop="deviceMessage">
+      </zm-table-column>
+      <zm-table-column label="触发的设备消息" align="center" prop="deviceMessage">
         <template #default="scope">
           <el-popover
             placement="top-start"
             :width="600"
             trigger="hover"
-            v-if="scope.row.deviceMessage"
-          >
+            v-if="scope.row.deviceMessage">
             <template #reference>
               <el-button link type="primary">
                 <Icon icon="ep:view" class="mr-5px" />
@@ -149,41 +132,38 @@
           </el-popover>
           <span v-else class="text-gray-400">-</span>
         </template>
-      </el-table-column>
-      <el-table-column label="是否处理" align="center" prop="processStatus">
+      </zm-table-column>
+      <zm-table-column label="是否处理" align="center" prop="processStatus">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.processStatus" />
         </template>
-      </el-table-column>
-      <el-table-column label="处理结果" align="center" prop="processRemark" />
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column label="处理结果" align="center" prop="processRemark" />
+      <zm-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="120px">
+        width="180px" />
+      <zm-table-column label="操作" align="center" min-width="120px" action>
         <template #default="scope">
           <el-button
             v-if="!scope.row.processStatus"
             link
             type="primary"
             @click="handleProcess(scope.row)"
-            v-hasPermi="['iot:alert-record:process']"
-          >
+            v-hasPermi="['iot:alert-record:process']">
             处理
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 </template>
 

+ 54 - 81
src/views/iot/device/device/index.vue

@@ -1,26 +1,23 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="产品" prop="productId">
         <el-select
           v-model="queryParams.productId"
           placeholder="请选择产品"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="product in products"
             :key="product.id"
             :label="product.name"
-            :value="product.id"
-          />
+            :value="product.id" />
         </el-select>
       </el-form-item>
       <el-form-item label="DeviceName" prop="deviceName">
@@ -29,8 +26,7 @@
           placeholder="请输入 DeviceName"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="备注名称" prop="nickname">
         <el-input
@@ -38,22 +34,19 @@
           placeholder="请输入备注名称"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="设备类型" prop="deviceType">
         <el-select
           v-model="queryParams.deviceType"
           placeholder="请选择设备类型"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="设备状态" prop="status">
@@ -61,14 +54,12 @@
           v-model="queryParams.status"
           placeholder="请选择设备状态"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="设备分组" prop="groupId">
@@ -76,14 +67,12 @@
           v-model="queryParams.groupId"
           placeholder="请选择设备分组"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="group in deviceGroups"
             :key="group.id"
             :label="group.name"
-            :value="group.id"
-          />
+            :value="group.id" />
         </el-select>
       </el-form-item>
       <el-form-item class="float-right !mr-0 !mb-0">
@@ -109,8 +98,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['iot:device:create']"
-        >
+          v-hasPermi="['iot:device:create']">
           <Icon icon="ep:plus" class="mr-5px" />
           新增
         </el-button>
@@ -119,8 +107,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['iot:device:export']"
-        >
+          v-hasPermi="['iot:device:export']">
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
         <el-button type="warning" plain @click="handleImport" v-hasPermi="['iot:device:import']">
@@ -131,8 +118,7 @@
           plain
           @click="openGroupForm"
           :disabled="selectedIds.length === 0"
-          v-hasPermi="['iot:device:update']"
-        >
+          v-hasPermi="['iot:device:update']">
           <Icon icon="ep:folder-add" class="mr-5px" /> 添加到分组
         </el-button>
         <el-button
@@ -140,8 +126,7 @@
           plain
           @click="handleDeleteList"
           :disabled="selectedIds.length === 0"
-          v-hasPermi="['iot:device:delete']"
-        >
+          v-hasPermi="['iot:device:delete']">
           <Icon icon="ep:delete" class="mr-5px" /> 批量删除
         </el-button>
       </el-form-item>
@@ -149,14 +134,13 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <template v-if="viewMode === 'card'">
       <el-row :gutter="16">
         <el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
           <el-card
             class="h-full transition-colors relative overflow-hidden"
-            :body-style="{ padding: '0' }"
-          >
+            :body-style="{ padding: '0' }">
             <!-- 添加渐变背景层 -->
             <div
               class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none"
@@ -164,8 +148,7 @@
                 item.state === DeviceStateEnum.ONLINE
                   ? 'bg-gradient-to-b from-[#eefaff] to-transparent'
                   : 'bg-gradient-to-b from-[#fff1f1] to-transparent'
-              ]"
-            >
+              ]">
             </div>
             <div class="p-4 relative">
               <!-- 标题区域 -->
@@ -173,7 +156,10 @@
                 <div class="mr-2.5 flex items-center">
                   <el-image :src="defaultIconUrl" class="w-[18px] h-[18px]" />
                 </div>
-                <div class="text-[16px] font-600 flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{{ item.deviceName }}</div>
+                <div
+                  class="text-[16px] font-600 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
+                  >{{ item.deviceName }}</div
+                >
                 <!-- 添加设备状态标签 -->
                 <div class="inline-flex items-center">
                   <div
@@ -182,13 +168,11 @@
                       item.state === DeviceStateEnum.ONLINE
                         ? 'bg-[var(--el-color-success)]'
                         : 'bg-[var(--el-color-danger)]'
-                    "
-                  >
+                    ">
                   </div>
                   <el-text
                     class="!text-xs font-bold"
-                    :type="item.state === DeviceStateEnum.ONLINE ? 'success' : 'danger'"
-                  >
+                    :type="item.state === DeviceStateEnum.ONLINE ? 'success' : 'danger'">
                     {{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATE, item.state) }}
                   </el-text>
                 </div>
@@ -210,8 +194,7 @@
                   <div class="mb-2.5 last:mb-0">
                     <span class="text-[#717c8e] mr-2.5">备注名称</span>
                     <span
-                      class="text-[var(--el-text-color-primary)] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[130px]"
-                    >
+                      class="text-[var(--el-text-color-primary)] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[130px]">
                       {{ item.nickname || item.deviceName }}
                     </span>
                   </div>
@@ -231,8 +214,7 @@
                   type="primary"
                   plain
                   @click="openForm('update', item.id)"
-                  v-hasPermi="['iot:device:update']"
-                >
+                  v-hasPermi="['iot:device:update']">
                   <Icon icon="ep:edit-pen" class="mr-1" />
                   编辑
                 </el-button>
@@ -240,8 +222,7 @@
                   class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
                   type="warning"
                   plain
-                  @click="openDetail(item.id)"
-                >
+                  @click="openDetail(item.id)">
                   <Icon icon="ep:view" class="mr-1" />
                   详情
                 </el-button>
@@ -249,8 +230,7 @@
                   class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
                   type="info"
                   plain
-                  @click="openModel(item.id)"
-                >
+                  @click="openModel(item.id)">
                   <Icon icon="ep:tickets" class="mr-1" />
                   数据
                 </el-button>
@@ -260,8 +240,7 @@
                   type="danger"
                   plain
                   @click="handleDelete(item.id)"
-                  v-hasPermi="['iot:device:delete']"
-                >
+                  v-hasPermi="['iot:device:delete']">
                   <Icon icon="ep:delete" />
                 </el-button>
               </div>
@@ -272,34 +251,33 @@
     </template>
 
     <!-- 列表视图 -->
-    <el-table
+    <zm-table
       v-else
-      v-loading="loading"
+      :loading="loading"
       :data="list"
       :stripe="true"
       :show-overflow-tooltip="true"
-      @selection-change="handleSelectionChange"
-    >
-      <el-table-column type="selection" width="55" />
-      <el-table-column label="DeviceName" align="center" prop="deviceName">
+      @selection-change="handleSelectionChange">
+      <zm-table-column type="selection" width="55" />
+      <zm-table-column label="DeviceName" align="center" prop="deviceName">
         <template #default="scope">
           <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
         </template>
-      </el-table-column>
-      <el-table-column label="备注名称" align="center" prop="nickname" />
-      <el-table-column label="所属产品" align="center" prop="productId">
+      </zm-table-column>
+      <zm-table-column label="备注名称" align="center" prop="nickname" />
+      <zm-table-column label="所属产品" align="center" prop="productId">
         <template #default="scope">
           <el-link @click="openProductDetail(scope.row.productId)">
             {{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
           </el-link>
         </template>
-      </el-table-column>
-      <el-table-column label="设备类型" align="center" prop="deviceType">
+      </zm-table-column>
+      <zm-table-column label="设备类型" align="center" prop="deviceType">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
         </template>
-      </el-table-column>
-      <el-table-column label="所属分组" align="center" prop="groupId">
+      </zm-table-column>
+      <zm-table-column label="所属分组" align="center" prop="groupId">
         <template #default="scope">
           <template v-if="scope.row.groupIds?.length">
             <el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
@@ -307,27 +285,25 @@
             </el-tag>
           </template>
         </template>
-      </el-table-column>
-      <el-table-column label="设备状态" align="center" prop="status">
+      </zm-table-column>
+      <zm-table-column label="设备状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
         </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         label="最后上线时间"
         align="center"
         prop="onlineTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="120px">
+        width="180px" />
+      <zm-table-column label="操作" align="center" min-width="90px" action>
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openDetail(scope.row.id)"
-            v-hasPermi="['iot:product:query']"
-          >
+            v-hasPermi="['iot:product:query']">
             查看
           </el-button>
           <el-button link type="primary" @click="openModel(scope.row.id)"> 日志 </el-button>
@@ -335,29 +311,26 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:device:update']"
-          >
+            v-hasPermi="['iot:device:update']">
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:device:delete']"
-          >
+            v-hasPermi="['iot:device:delete']">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
 
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 21 - 29
src/views/iot/device/group/index.vue

@@ -1,21 +1,19 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="分组名字" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入分组名字"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
@@ -25,8 +23,7 @@
           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>
@@ -35,8 +32,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['iot:device-group:create']"
-        >
+          v-hasPermi="['iot:device-group:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
       </el-form-item>
@@ -44,52 +40,48 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="分组 ID" align="center" prop="id" />
-      <el-table-column label="分组名字" align="center" prop="name" />
-      <el-table-column label="分组状态" align="center" prop="status">
+  <ContentWrap style="border: none">
+    <zm-table :loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <zm-table-column label="分组 ID" align="center" prop="id" />
+      <zm-table-column label="分组名字" align="center" prop="name" />
+      <zm-table-column label="分组状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
-      </el-table-column>
-      <el-table-column label="分组描述" align="center" prop="description" />
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column label="分组描述" align="center" prop="description" />
+      <zm-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="设备数量" align="center" prop="deviceCount" />
-      <el-table-column label="操作" align="center" min-width="120px">
+        width="180px" />
+      <zm-table-column label="设备数量" align="center" prop="deviceCount" />
+      <zm-table-column label="操作" align="center" width="120px" action>
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:device-group:update']"
-          >
+            v-hasPermi="['iot:device-group:update']">
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:device-group:delete']"
-          >
+            v-hasPermi="['iot:device-group:delete']">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 8 - 4
src/views/iot/home/components/ComparisonCard.vue

@@ -40,11 +40,15 @@ const props = defineProps({
 
 <style lang="scss" scoped>
 .stat-card {
-  transition: all 0.3s;
+  height: 100%;
 
-  &:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 5px 15px rgb(0 0 0 / 8%);
+  :deep(.el-card__body) {
+    padding: 24px;
+  }
+
+  :deep(.el-divider) {
+    margin: 12px 0;
+    border-color: #f0f0f0;
   }
 }
 </style>

+ 15 - 0
src/views/iot/home/components/DeviceCountCard.vue

@@ -129,3 +129,18 @@ onMounted(async () => {
   })
 })
 </script>
+
+<style lang="scss" scoped>
+.chart-card {
+  height: 100%;
+
+  :deep(.el-card__header) {
+    padding: 16px 20px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+
+  :deep(.el-card__body) {
+    padding: 20px;
+  }
+}
+</style>

+ 27 - 8
src/views/iot/home/index.vue

@@ -8,8 +8,7 @@
         :todayCount="statsData.productCategoryTodayCount"
         icon="ep:menu"
         iconColor="text-blue-400"
-        :loading="loading"
-      />
+        :loading="loading" />
     </el-col>
     <el-col :span="6">
       <ComparisonCard
@@ -18,8 +17,7 @@
         :todayCount="statsData.productTodayCount"
         icon="ep:box"
         iconColor="text-orange-400"
-        :loading="loading"
-      />
+        :loading="loading" />
     </el-col>
     <el-col :span="6">
       <ComparisonCard
@@ -28,8 +26,7 @@
         :todayCount="statsData.deviceTodayCount"
         icon="ep:cpu"
         iconColor="text-purple-400"
-        :loading="loading"
-      />
+        :loading="loading" />
     </el-col>
     <el-col :span="6">
       <ComparisonCard
@@ -38,8 +35,7 @@
         :todayCount="statsData.deviceMessageTodayCount"
         icon="ep:message"
         iconColor="text-teal-400"
-        :loading="loading"
-      />
+        :loading="loading" />
     </el-col>
   </el-row>
 
@@ -112,3 +108,26 @@ onMounted(() => {
   getStats()
 })
 </script>
+
+<style lang="scss" scoped>
+:deep(.el-card) {
+  border: none !important;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  border-radius: 8px;
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
+    transform: translateY(-2px);
+  }
+
+  .el-card__header {
+    border-bottom: 1px solid #f0f0f0;
+    padding: 16px 20px;
+  }
+
+  .el-card__body {
+    padding: 20px;
+  }
+}
+</style>

+ 29 - 42
src/views/iot/ota/firmware/index.vue

@@ -1,21 +1,19 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="固件名称" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入固件名称"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="产品" prop="productId">
         <el-select
@@ -23,14 +21,12 @@
           placeholder="请选择产品"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="product in productList"
             :key="product.id"
             :label="product.name"
-            :value="product.id"
-          />
+            :value="product.id" />
         </el-select>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -41,8 +37,7 @@
           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>
@@ -51,8 +46,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['iot:ota-firmware:create']"
-        >
+          v-hasPermi="['iot:ota-firmware:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
       </el-form-item>
@@ -60,79 +54,72 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table
+  <ContentWrap style="border: none">
+    <zm-table
       row-key="id"
-      v-loading="loading"
+      :loading="loading"
       :data="list"
       :stripe="true"
-      :show-overflow-tooltip="true"
-    >
-      <el-table-column label="固件编号" align="center" prop="id" />
-      <el-table-column label="固件名称" align="center" prop="name" />
-      <el-table-column label="固件版本" align="center" prop="description" />
-      <el-table-column label="版本号" align="center" prop="version" />
-      <el-table-column label="所属产品" align="center" prop="productId">
+      :show-overflow-tooltip="true">
+      <zm-table-column label="固件编号" align="center" prop="id" />
+      <zm-table-column label="固件名称" align="center" prop="name" />
+      <zm-table-column label="固件版本" align="center" prop="description" />
+      <zm-table-column label="版本号" align="center" prop="version" />
+      <zm-table-column label="所属产品" align="center" prop="productId">
         <template #default="scope">
           <el-link
             @click="openProductDetail(scope.row.productId)"
-            v-if="getProductName(scope.row.productId)"
-          >
+            v-if="getProductName(scope.row.productId)">
             {{ getProductName(scope.row.productId) }}
           </el-link>
           <span v-else>加载中...</span>
         </template>
-      </el-table-column>
-      <el-table-column label="固件文件" align="center" prop="fileUrl">
+      </zm-table-column>
+      <zm-table-column label="固件文件" align="center" prop="fileUrl">
         <template #default="scope">
           <el-link :href="scope.row.fileUrl" target="_blank" download>
             <Icon icon="ep:download" class="mr-5px" />
             下载固件
           </el-link>
         </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="180px">
+        width="180px" />
+      <zm-table-column label="操作" align="center" min-width="180px" action>
         <template #default="scope">
           <el-button
             link
             @click="openFirmwareDetail(scope.row.id)"
-            v-hasPermi="['iot:ota-firmware:query']"
-          >
+            v-hasPermi="['iot:ota-firmware:query']">
             详情
           </el-button>
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:ota-firmware:update']"
-          >
+            v-hasPermi="['iot:ota-firmware:update']">
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:ota-firmware:delete']"
-          >
+            v-hasPermi="['iot:ota-firmware:delete']">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 21 - 29
src/views/iot/product/category/index.vue

@@ -1,21 +1,19 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="分类名字" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入分类名字"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
@@ -25,8 +23,7 @@
           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>
@@ -35,8 +32,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['iot:product-category:create']"
-        >
+          v-hasPermi="['iot:product-category:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
       </el-form-item>
@@ -44,52 +40,48 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="ID" align="center" prop="id" />
-      <el-table-column label="名字" align="center" prop="name" />
-      <el-table-column label="排序" align="center" prop="sort" />
-      <el-table-column label="状态" align="center" prop="status">
+  <ContentWrap style="border: none">
+    <zm-table :loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <zm-table-column label="ID" align="center" prop="id" />
+      <zm-table-column label="名字" align="center" prop="name" />
+      <zm-table-column label="排序" align="center" prop="sort" />
+      <zm-table-column label="状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
-      </el-table-column>
-      <el-table-column label="描述" align="center" prop="description" />
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column label="描述" align="center" prop="description" />
+      <zm-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="120px">
+        width="180px" />
+      <zm-table-column label="操作" align="center" width="120px" action>
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:product-category:update']"
-          >
+            v-hasPermi="['iot:product-category:update']">
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:product-category:delete']"
-          >
+            v-hasPermi="['iot:product-category:delete']">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 34 - 49
src/views/iot/product/product/index.vue

@@ -1,21 +1,19 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       ref="queryFormRef"
       :inline="true"
       :model="queryParams"
       class="-mb-15px"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="产品名称" prop="name">
         <el-input
           v-model="queryParams.name"
           class="!w-240px"
           clearable
           placeholder="请输入产品名称"
-          @keyup.enter="handleQuery"
-        />
+          @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item label="ProductKey" prop="productKey">
         <el-input
@@ -23,8 +21,7 @@
           class="!w-240px"
           clearable
           placeholder="请输入产品标识"
-          @keyup.enter="handleQuery"
-        />
+          @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
@@ -39,8 +36,7 @@
           v-hasPermi="['iot:product:create']"
           plain
           type="primary"
-          @click="openForm('create')"
-        >
+          @click="openForm('create')">
           <Icon class="mr-5px" icon="ep:plus" />
           新增
         </el-button>
@@ -49,8 +45,7 @@
           :loading="exportLoading"
           plain
           type="success"
-          @click="handleExport"
-        >
+          @click="handleExport">
           <Icon class="mr-5px" icon="ep:download" />
           导出
         </el-button>
@@ -70,7 +65,7 @@
   </ContentWrap>
 
   <!-- 卡片视图 -->
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <el-row v-if="viewMode === 'card'" :gutter="16">
       <el-col v-for="item in list" :key="item.id" :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
         <el-card :body-style="{ padding: '0' }" class="h-full transition-colors">
@@ -97,7 +92,8 @@
                 </div>
                 <div class="mb-2.5 last:mb-0">
                   <span class="text-[#717c8e] mr-2.5">产品标识</span>
-                  <span class="text-[var(--el-text-color-primary)] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
+                  <span
+                    class="text-[var(--el-text-color-primary)] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
                     {{ item.productKey }}
                   </span>
                 </div>
@@ -117,8 +113,7 @@
                 class="flex-1 !px-2 !h-[32px] text-[13px]"
                 plain
                 type="primary"
-                @click="openForm('update', item.id)"
-              >
+                @click="openForm('update', item.id)">
                 <Icon class="mr-1" icon="ep:edit-pen" />
                 编辑
               </el-button>
@@ -126,8 +121,7 @@
                 class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
                 plain
                 type="warning"
-                @click="openDetail(item.id)"
-              >
+                @click="openDetail(item.id)">
                 <Icon class="mr-1" icon="ep:view" />
                 详情
               </el-button>
@@ -135,8 +129,7 @@
                 class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
                 plain
                 type="success"
-                @click="openObjectModel(item)"
-              >
+                @click="openObjectModel(item)">
                 <Icon class="mr-1" icon="ep:scale-to-original" />
                 物模型
               </el-button>
@@ -147,8 +140,7 @@
                 class="!px-2 !h-[32px] text-[13px]"
                 plain
                 type="danger"
-                @click="handleDelete(item.id)"
-              >
+                @click="handleDelete(item.id)">
                 <Icon icon="ep:delete" />
               </el-button>
             </div>
@@ -158,60 +150,55 @@
     </el-row>
 
     <!-- 列表视图 -->
-    <el-table v-else v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="ID" prop="id" />
-      <el-table-column align="center" label="ProductKey" prop="productKey" />
-      <el-table-column align="center" label="品类" prop="categoryName" />
-      <el-table-column align="center" label="设备类型" prop="deviceType">
+    <zm-table v-else :loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <zm-table-column align="center" label="ID" prop="id" />
+      <zm-table-column align="center" label="ProductKey" prop="productKey" />
+      <zm-table-column align="center" label="品类" prop="categoryName" />
+      <zm-table-column align="center" label="设备类型" prop="deviceType">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
         </template>
-      </el-table-column>
-      <el-table-column align="center" label="产品图标" prop="icon">
+      </zm-table-column>
+      <zm-table-column align="center" label="产品图标" prop="icon">
         <template #default="scope">
           <el-image
             v-if="scope.row.icon"
             :preview-src-list="[scope.row.icon]"
             :src="scope.row.icon"
-            class="w-40px h-40px"
-          />
+            class="w-40px h-40px" />
           <span v-else>-</span>
         </template>
-      </el-table-column>
-      <el-table-column align="center" label="产品图片" prop="picture">
+      </zm-table-column>
+      <zm-table-column align="center" label="产品图片" prop="picture">
         <template #default="scope">
           <el-image
             v-if="scope.row.picUrl"
             :preview-src-list="[scope.row.picture]"
             :src="scope.row.picUrl"
-            class="w-40px h-40px"
-          />
+            class="w-40px h-40px" />
           <span v-else>-</span>
         </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         :formatter="dateFormatter"
         align="center"
         label="创建时间"
         prop="createTime"
-        width="180px"
-      />
-      <el-table-column align="center" label="操作">
+        width="180px" />
+      <zm-table-column align="center" label="操作" action>
         <template #default="scope">
           <el-button
             v-hasPermi="['iot:product:query']"
             link
             type="primary"
-            @click="openDetail(scope.row.id)"
-          >
+            @click="openDetail(scope.row.id)">
             查看
           </el-button>
           <el-button
             v-hasPermi="['iot:product:update']"
             link
             type="primary"
-            @click="openForm('update', scope.row.id)"
-          >
+            @click="openForm('update', scope.row.id)">
             编辑
           </el-button>
           <el-button
@@ -219,21 +206,19 @@
             :disabled="scope.row.status === 1"
             link
             type="danger"
-            @click="handleDelete(scope.row.id)"
-          >
+            @click="handleDelete(scope.row.id)">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
 
     <!-- 分页 -->
     <Pagination
       v-model:limit="queryParams.pageSize"
       v-model:page="queryParams.pageNo"
       :total="total"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 1 - 1
src/views/iot/rule/data/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-tabs v-model="activeTab" type="border-card">
+  <el-tabs v-model="activeTab">
     <el-tab-pane label="规则" name="rule">
       <RuleIndex />
     </el-tab-pane>

+ 28 - 39
src/views/iot/rule/data/rule/index.vue

@@ -1,35 +1,31 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       class="-mb-15px"
       :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="规则名称" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入规则名称"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="规则状态" prop="status">
         <el-select
           v-model="queryParams.status"
           placeholder="请选择规则状态"
           clearable
-          class="!w-240px"
-        >
+          class="!w-240px">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -40,8 +36,7 @@
           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>
@@ -50,8 +45,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['iot:data-rule:create']"
-        >
+          v-hasPermi="['iot:data-rule:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
       </el-form-item>
@@ -59,63 +53,58 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table
+  <ContentWrap style="border: none">
+    <zm-table
       row-key="id"
-      v-loading="loading"
+      :loading="loading"
       :data="list"
       :stripe="true"
-      :show-overflow-tooltip="true"
-    >
-      <el-table-column label="规则编号" align="center" prop="id" />
-      <el-table-column label="规则名称" align="center" prop="name" />
-      <el-table-column label="规则描述" align="center" prop="description" />
-      <el-table-column label="规则状态" align="center" prop="status">
+      :show-overflow-tooltip="true">
+      <zm-table-column label="规则编号" align="center" prop="id" />
+      <zm-table-column label="规则名称" align="center" prop="name" />
+      <zm-table-column label="规则描述" align="center" prop="description" />
+      <zm-table-column label="规则状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
-      </el-table-column>
-      <el-table-column label="数据源" align="center" prop="sourceConfigs">
+      </zm-table-column>
+      <zm-table-column label="数据源" align="center" prop="sourceConfigs">
         <template #default="scope"> {{ scope.row.sourceConfigs?.length || 0 }} 个 </template>
-      </el-table-column>
-      <el-table-column label="数据目的" align="center" prop="sinkIds">
+      </zm-table-column>
+      <zm-table-column label="数据目的" align="center" prop="sinkIds">
         <template #default="scope"> {{ scope.row.sinkIds?.length || 0 }} 个 </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center" min-width="120px">
+        width="180px" />
+      <zm-table-column label="操作" align="center" min-width="120px" action>
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:data-rule:update']"
-          >
+            v-hasPermi="['iot:data-rule:update']">
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:data-rule:delete']"
-          >
+            v-hasPermi="['iot:data-rule:delete']">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 26 - 38
src/views/iot/rule/data/sink/index.vue

@@ -1,35 +1,31 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 搜索工作栏 -->
     <el-form
       ref="queryFormRef"
       :inline="true"
       :model="queryParams"
       class="-mb-15px"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="目的名称" prop="name">
         <el-input
           v-model="queryParams.name"
           class="!w-240px"
           clearable
           placeholder="请输入目的名称"
-          @keyup.enter="handleQuery"
-        />
+          @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item label="目的状态" prop="status">
         <el-select
           v-model="queryParams.status"
           class="!w-240px"
           clearable
-          placeholder="请选择目的状态"
-        >
+          placeholder="请选择目的状态">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="目的类型" prop="type">
@@ -37,14 +33,12 @@
           v-model="queryParams.type"
           class="!w-240px"
           clearable
-          placeholder="请选择目的类型"
-        >
+          placeholder="请选择目的类型">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -55,8 +49,7 @@
           end-placeholder="结束日期"
           start-placeholder="开始日期"
           type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
+          value-format="YYYY-MM-DD HH:mm:ss" />
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
@@ -71,8 +64,7 @@
           v-hasPermi="['iot:data-sink:create']"
           plain
           type="primary"
-          @click="openForm('create')"
-        >
+          @click="openForm('create')">
           <Icon class="mr-5px" icon="ep:plus" />
           新增
         </el-button>
@@ -81,56 +73,52 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="目的编号" prop="id" />
-      <el-table-column align="center" label="目的名称" prop="name" />
-      <el-table-column align="center" label="目的描述" prop="description" />
-      <el-table-column align="center" label="目的状态" prop="status">
+  <ContentWrap style="border: none">
+    <zm-table :loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <zm-table-column align="center" label="目的编号" prop="id" />
+      <zm-table-column align="center" label="目的名称" prop="name" />
+      <zm-table-column align="center" label="目的描述" prop="description" />
+      <zm-table-column align="center" label="目的状态" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
-      </el-table-column>
-      <el-table-column align="center" label="目的类型" prop="type">
+      </zm-table-column>
+      <zm-table-column align="center" label="目的类型" prop="type">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM" :value="scope.row.type" />
         </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         :formatter="dateFormatter"
         align="center"
         label="创建时间"
         prop="createTime"
-        width="180px"
-      />
-      <el-table-column align="center" fixed="right" label="操作" width="120px">
+        width="180px" />
+      <zm-table-column align="center" fixed="right" label="操作" width="120px" action>
         <template #default="scope">
           <el-button
             v-hasPermi="['iot:data-sink:update']"
             link
             type="primary"
-            @click="openForm('update', scope.row.id)"
-          >
+            @click="openForm('update', scope.row.id)">
             编辑
           </el-button>
           <el-button
             v-hasPermi="['iot:data-sink:delete']"
             link
             type="danger"
-            @click="handleDelete(scope.row.id)"
-          >
+            @click="handleDelete(scope.row.id)">
             删除
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       v-model:limit="queryParams.pageSize"
       v-model:page="queryParams.pageNo"
       :total="total"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->

+ 36 - 46
src/views/iot/rule/scene/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <ContentWrap>
+  <ContentWrap style="border: none">
     <!-- 页面头部 -->
     <div class="flex justify-between items-start mb-20px">
       <div class="flex-1">
@@ -20,36 +20,32 @@
     </div>
 
     <!-- 搜索和筛选 -->
-    <el-card class="mb-16px" shadow="never">
+    <el-card class="mb-16px" shadow="never" style="border: none">
       <el-form
         ref="queryFormRef"
         :model="queryParams"
         :inline="true"
         label-width="80px"
-        @submit.prevent
-      >
+        @submit.prevent>
         <el-form-item label="规则名称">
           <el-input
             v-model="queryParams.name"
             placeholder="请输入规则名称"
             clearable
             @keyup.enter="handleQuery"
-            class="!w-240px"
-          />
+            class="!w-240px" />
         </el-form-item>
         <el-form-item label="规则状态">
           <el-select
             v-model="queryParams.status"
             placeholder="请选择状态"
             clearable
-            class="!w-240px"
-          >
+            class="!w-240px">
             <el-option
               v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
               :key="dict.value"
               :label="dict.label"
-              :value="dict.value"
-            />
+              :value="dict.value" />
           </el-select>
         </el-form-item>
         <el-form-item>
@@ -69,13 +65,12 @@
     <el-row :gutter="16" class="mb-16px">
       <el-col :span="6">
         <el-card
+          style="border: none"
           class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
-          shadow="hover"
-        >
+          shadow="hover">
           <div class="flex items-center">
             <div
-              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#667eea] to-[#764ba2]"
-            >
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#667eea] to-[#764ba2]">
               <Icon icon="ep:document" />
             </div>
             <div>
@@ -89,13 +84,12 @@
       </el-col>
       <el-col :span="6">
         <el-card
+          style="border: none"
           class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
-          shadow="hover"
-        >
+          shadow="hover">
           <div class="flex items-center">
             <div
-              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#f093fb] to-[#f5576c]"
-            >
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#f093fb] to-[#f5576c]">
               <Icon icon="ep:check" />
             </div>
             <div>
@@ -109,13 +103,12 @@
       </el-col>
       <el-col :span="6">
         <el-card
+          style="border: none"
           class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
-          shadow="hover"
-        >
+          shadow="hover">
           <div class="flex items-center">
             <div
-              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#4facfe] to-[#00f2fe]"
-            >
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#4facfe] to-[#00f2fe]">
               <Icon icon="ep:close" />
             </div>
             <div>
@@ -129,13 +122,12 @@
       </el-col>
       <el-col :span="6">
         <el-card
+          style="border: none"
           class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
-          shadow="hover"
-        >
+          shadow="hover">
           <div class="flex items-center">
             <div
-              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#43e97b] to-[#38f9d7]"
-            >
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#43e97b] to-[#38f9d7]">
               <Icon icon="ep:timer" />
             </div>
             <div>
@@ -150,10 +142,10 @@
     </el-row>
 
     <!-- 数据表格 -->
-    <el-card class="mb-20px" shadow="never">
-      <el-table v-loading="loading" :data="list" stripe @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" />
-        <el-table-column label="规则名称" prop="name" min-width="200">
+    <el-card class="mb-20px" shadow="never" style="border: none">
+      <zm-table :loading="loading" :data="list" stripe @selection-change="handleSelectionChange">
+        <zm-table-column type="selection" width="55" />
+        <zm-table-column label="规则名称" prop="name" min-width="200">
           <template #default="{ row }">
             <div class="flex items-center gap-8px">
               <span class="font-500 text-[#303133]">{{ row.name }}</span>
@@ -163,9 +155,9 @@
               {{ row.description }}
             </div>
           </template>
-        </el-table-column>
+        </zm-table-column>
         <!-- 触发条件列 -->
-        <el-table-column label="触发条件" min-width="280">
+        <zm-table-column label="触发条件" min-width="280">
           <template #default="{ row }">
             <div class="space-y-4px">
               <div class="flex flex-wrap gap-4px">
@@ -188,9 +180,9 @@
               </div>
             </div>
           </template>
-        </el-table-column>
+        </zm-table-column>
         <!-- 执行动作列 -->
-        <el-table-column label="执行动作" min-width="250">
+        <zm-table-column label="执行动作" min-width="250">
           <template #default="{ row }">
             <div class="flex flex-wrap gap-4px">
               <el-tag type="success" size="small" class="m-0">
@@ -198,21 +190,21 @@
               </el-tag>
             </div>
           </template>
-        </el-table-column>
-        <el-table-column label="最近触发" prop="lastTriggeredTime" width="180">
+        </zm-table-column>
+        <zm-table-column label="最近触发" prop="lastTriggeredTime" width="180">
           <template #default="{ row }">
             <span v-if="row.lastTriggeredTime">
               {{ formatDate(row.lastTriggeredTime) }}
             </span>
             <span v-else class="text-gray-400">未触发</span>
           </template>
-        </el-table-column>
-        <el-table-column label="创建时间" prop="createTime" width="180">
+        </zm-table-column>
+        <zm-table-column label="创建时间" prop="createTime" width="180">
           <template #default="{ row }">
             {{ formatDate(row.createTime) }}
           </template>
-        </el-table-column>
-        <el-table-column label="操作" width="210" fixed="right">
+        </zm-table-column>
+        <zm-table-column label="操作" width="210" fixed="right" action>
           <template #default="{ row }">
             <div>
               <el-button type="primary" link @click="handleEdit(row)">
@@ -222,8 +214,7 @@
               <el-button
                 :type="row.status === 0 ? 'warning' : 'success'"
                 link
-                @click="handleToggleStatus(row)"
-              >
+                @click="handleToggleStatus(row)">
                 <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
                 {{ getDictLabel(DICT_TYPE.COMMON_STATUS, row.status) }}
               </el-button>
@@ -233,16 +224,15 @@
               </el-button>
             </div>
           </template>
-        </el-table-column>
-      </el-table>
+        </zm-table-column>
+      </zm-table>
 
       <!-- 分页 -->
       <Pagination
         :total="total"
         v-model:page="queryParams.pageNo"
         v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
+        @pagination="getList" />
     </el-card>
 
     <!-- 表单对话框 -->

+ 240 - 53
src/views/pms/qhse/certificate.vue

@@ -20,16 +20,14 @@
               v-model="queryParams.classify"
               placeholder="证书类别"
               clearable
-              class="!w-150px"
-            >
+              class="!w-150px">
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT).concat(
                   getStrDictOptions(DICT_TYPE.ORG_CERT)
                 )"
                 :key="dict.value"
                 :label="dict.label"
-                :value="dict.value"
-              />
+                :value="dict.value" />
             </el-select>
           </el-form-item>
 
@@ -42,14 +40,12 @@
               v-model="queryParams.expired"
               placeholder="请选择是否过期"
               clearable
-              style="width: 150px"
-            >
+              style="width: 150px">
               <el-option
                 v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
                 :key="dict.value"
                 :label="dict.label"
-                :value="dict.value"
-              />
+                :value="dict.value" />
             </el-select>
           </el-form-item>
 
@@ -72,13 +68,87 @@
 
       <!-- 列表 -->
       <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
+        <div class="stats-cards">
+          <div class="stats-card stats-card--expired">
+            <div class="flex items-center gap-2">
+              <Icon icon="ep:info-filled" color="#de3b3b" />
+              <div class="stats-card__label">已过期</div>
+            </div>
+
+            <div class="stats-card__value">
+              <CountTo
+                :duration="2600"
+                :end-val="expired"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
+            </div>
+          </div>
+          <div class="stats-card stats-card--warn">
+            <div class="flex items-center gap-2">
+              <Icon icon="ep:bell-filled" color="#d97706" />
+              <div class="stats-card__label">60天预警</div>
+            </div>
+            <div class="stats-card__value">
+              <CountTo
+                :duration="2600"
+                :end-val="warn"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#d97706]!" />
+            </div>
+          </div>
+          <div class="stats-card stats-card--total">
+            <div class="flex items-center gap-2">
+              <Icon icon="eos-icons:counting" color="#2563eb" />
+              <div class="stats-card__label">证书总数</div>
+            </div>
+
+            <div class="stats-card__value">
+              <CountTo
+                :duration="2600"
+                :end-val="totalCert"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
+            </div>
+          </div>
+          <div class="stats-card stats-card--personal">
+            <div class="flex items-center gap-2">
+              <Icon
+                icon="material-symbols-light:account-circle-outline"
+                class="w-20 h-20"
+                color="#2563eb" />
+              <div class="stats-card__label">个人证书</div>
+            </div>
+
+            <div class="stats-card__value">
+              <CountTo
+                :duration="2600"
+                :end-val="personal"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
+            </div>
+          </div>
+          <div class="stats-card stats-card--organization">
+            <div class="flex items-center gap-2">
+              <Icon icon="bxs:building" class="w-20 h-20" color="#2563eb" />
+              <div class="stats-card__label">组织证书</div>
+            </div>
+            <div class="stats-card__value">
+              <CountTo
+                :duration="2600"
+                :end-val="organization"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
+            </div>
+          </div>
+        </div>
         <zm-table
           :loading="loading"
           :data="list"
-          height="calc(80vh - 220px)"
+          height="calc(62.5vh - 220px)"
           :show-overflow-tooltip="true"
           :row-style="tableRowStyle"
-        >
+          :row-class-name="tableRowClassName">
+          >
           <zm-table-column :label="t('monitor.serial')" width="70" align="center">
             <template #default="scope">
               {{ scope.$index + 1 }}
@@ -96,8 +166,7 @@
               <dict-tag
                 v-if="scope.row.type === 'organization'"
                 :type="DICT_TYPE.ORG_CERT"
-                :value="scope.row.classify"
-              />
+                :value="scope.row.classify" />
               <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
             </template>
           </zm-table-column>
@@ -132,8 +201,7 @@
             align="center"
             fixed="right"
             min-width="180px"
-            action
-          >
+            action>
             <template #default="scope">
               <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
               <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
@@ -141,8 +209,7 @@
                 link
                 type="success"
                 v-if="scope.row.certPic"
-                @click="handleViewImage(scope.row.certPic)"
-              >
+                @click="handleViewImage(scope.row.certPic)">
                 查看证书
               </el-button>
             </template>
@@ -154,8 +221,7 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+          @pagination="getList" />
       </ContentWrap>
 
       <ContentWrap style="margin-top: -5px">
@@ -164,6 +230,17 @@
             <Bell />
           </template>
         </el-alert>
+
+        <el-alert
+          title="证书60天橙色预警"
+          type="warning"
+          show-icon
+          :closable="false"
+          style="margin-top: 5px">
+          <template #icon>
+            <Bell />
+          </template>
+        </el-alert>
       </ContentWrap>
     </el-col>
   </el-row>
@@ -174,21 +251,18 @@
     v-model="dialogVisible"
     width="600px"
     destroy-on-close
-    @close="closeDialog"
-  >
+    @close="closeDialog">
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
       label-width="120px"
-      v-loading="formLoading"
-    >
+      v-loading="formLoading">
       <el-form-item label="证书类型" prop="type">
         <el-select
           v-model="formData.type"
           placeholder="请选择证书类型"
-          @change="formData.classify = ''"
-        >
+          @change="formData.classify = ''">
           <el-option label="个人证书" value="personal" />
           <el-option label="组织证书" value="organization" />
           <el-option label="其他" value="other" />
@@ -201,22 +275,19 @@
           v-if="formData.type === 'personal'"
           v-model="formData.classify"
           placeholder="证书类别"
-          clearable
-        >
+          clearable>
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
         <el-select v-else v-model="formData.classify" placeholder="证书类别" clearable>
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.ORG_CERT)"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
-          />
+            :value="dict.value" />
         </el-select>
       </el-form-item>
 
@@ -234,8 +305,7 @@
           node-key="id"
           filterable
           placeholder="请选择所在部门"
-          @change="handleDeptChange"
-        />
+          @change="handleDeptChange" />
       </el-form-item>
 
       <el-form-item label="所属人" prop="userName">
@@ -256,8 +326,7 @@
           type="date"
           value-format="x"
           placeholder="请选择颁发时间"
-          style="width: 100%"
-        />
+          style="width: 100%" />
       </el-form-item>
 
       <el-form-item label="有效期" prop="certExpire">
@@ -266,8 +335,7 @@
           type="date"
           value-format="x"
           placeholder="请选择有效期"
-          style="width: 100%"
-        />
+          style="width: 100%" />
       </el-form-item>
 
       <el-form-item label="到期前提醒" prop="noticeBefore">
@@ -276,8 +344,7 @@
           :min="0"
           :max="365"
           placeholder="请输入提前多少天提醒"
-          style="width: 100%"
-        />
+          style="width: 100%" />
       </el-form-item>
 
       <el-form-item label="备注" prop="remark">
@@ -286,8 +353,7 @@
           v-model="formData.remark"
           :rows="2"
           placeholder="请输入备注"
-          style="width: 100%"
-        />
+          style="width: 100%" />
       </el-form-item>
 
       <el-form-item label="证书图片" prop="certPic">
@@ -305,15 +371,13 @@
   <el-dialog :title="imageDialogTitle" v-model="imageDialogVisible" width="800px" center>
     <!-- <img :src="imagePreviewUrl" alt="证书图片" style="max-width: 100%; max-height: 80vh" /> -->
     <div
-      style="display: flex; justify-content: center; align-items: center; flex-direction: column"
-    >
+      style="display: flex; justify-content: center; align-items: center; flex-direction: column">
       <img
         v-for="url in imagePreviewUrl"
         :src="url"
         :key="url"
         alt="证书图片"
-        style="max-width: 100%"
-      />
+        style="max-width: 100%" />
     </div>
   </el-dialog>
 </template>
@@ -331,6 +395,8 @@ import UploadImage from '@/components/UploadFile/src/QHSEUploadImgs.vue'
 import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
 import { defaultProps } from '@/utils/tree'
 import { selectedDeptsEmployee } from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+const userStore = useUserStore()
 
 defineOptions({ name: 'IotQHSECertificate' })
 
@@ -463,6 +529,7 @@ const handleExport = async () => {
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   await getList()
+  await getStatic()
 }
 
 /** 搜索按钮操作 */
@@ -568,9 +635,22 @@ const tableRowStyle = ({ row }) => {
   if (row.expired) {
     return { backgroundColor: '#ffe6e6' }
   }
+  if (row.alertWarn) {
+    return { backgroundColor: '#e19f1a' }
+  }
   return {}
 }
 
+const tableRowClassName = ({ row }) => {
+  if (row.expired) {
+    return 'expired-row'
+  }
+  if (row.alertWarn) {
+    return 'alert-warn-row'
+  }
+  return ''
+}
+
 // 提交表单
 const submitForm = async () => {
   if (!formRef.value) return
@@ -645,32 +725,139 @@ const handleDeptChange = async (value) => {
   console.log('value>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', userList.value)
 }
 
+let totalCert = ref(0)
+let expired = ref(0)
+let warn = ref(0)
+let personal = ref(0)
+let organization = ref(0)
+async function getStatic() {
+  if (queryParams.deptId) {
+    const res = await IotMeasureCertApi.getIotMeasureCertStatistics(queryParams.deptId)
+    totalCert.value = res.total
+    expired.value = res.expired
+    warn.value = res.warn
+    personal.value = res.personal
+    organization.value = res.organization
+  } else {
+    const res = await IotMeasureCertApi.getIotMeasureCertStatistics(userStore.user.deptId)
+    totalCert.value = res.total
+    expired.value = res.expired
+    warn.value = res.warn
+    personal.value = res.personal
+    organization.value = res.organization
+  }
+}
 onMounted(async () => {
   getList()
-
+  getStatic()
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
 })
 </script>
 
 <style scoped>
-::deep(.el-tree--highlight-current) {
+:deep(.el-tree--highlight-current) {
   height: 200px !important;
 }
-::deep(.el-transfer-panel__body) {
+:deep(.el-transfer-panel__body) {
   height: 700px !important;
 }
-.image-preview {
-  margin-top: 10px;
-  display: flex;
-  justify-content: center;
+
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.stats-card {
+  padding: 14px 16px;
+  background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
+  border: 1px solid #e4ecf7;
+  border-radius: 10px;
+  box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
+}
+
+.stats-card__label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+.stats-card__value {
+  margin-top: 8px;
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1;
+  color: #1f5bb8;
+}
+
+.stats-card--expired {
+  background: linear-gradient(180deg, #fff4f4 0%, #ffe8e8 100%);
+  border-color: #ffcfcf;
+}
+
+.stats-card--expired .stats-card__value {
+  color: #de3b3b;
+}
+
+.stats-card--warn {
+  background: linear-gradient(180deg, #fff8ef 0%, #ffeed9 100%);
+  border-color: #ffd7a1;
+}
+
+.stats-card--warn .stats-card__value {
+  color: #d97706;
 }
 
-::deep(.el-table__body tr.expired-row) {
+.stats-card--total .stats-card__value {
+  color: #2563eb;
+}
+
+.stats-card--personal .stats-card__value {
+  color: #16a34a;
+}
+
+.stats-card--organization .stats-card__value {
+  color: #7c3aed;
+}
+
+/* 过期行的红色背景 - 基础状态 */
+:deep(.el-table__body tr.expired-row > td.el-table__cell) {
   background-color: #ffe6e6 !important;
 }
 
-::deep(.el-table__body tr.expired-row:hover td) {
+/* 过期行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.expired-row:hover > td.el-table__cell) {
   background-color: #ffcccc !important;
 }
+
+/* 确保斑马纹不影响过期行 */
+:deep(.el-table__body tr.expired-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+:deep(.el-table__body tr.expired-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 预警行的橙色背景 - 基础状态 */
+:deep(.el-table__body tr.alert-warn-row > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+/* 预警行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.alert-warn-row:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
+
+/* 确保斑马纹不影响预警行 */
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
 </style>

+ 3 - 0
src/views/pms/qhse/hiddenClass/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div>分类</div>
+</template>

+ 387 - 42
src/views/pms/qhse/index.vue

@@ -14,22 +14,19 @@
               placeholder="请输入计量器具名称"
               clearable
               @keyup.enter="handleQuery"
-              class="!w-180px"
-            />
+              class="!w-180px" />
           </el-form-item>
           <el-form-item label="是否过期" prop="expired">
             <el-select
               v-model="queryParams.expired"
               placeholder="请选择是否过期"
               clearable
-              style="width: 180px"
-            >
+              style="width: 180px">
               <el-option
                 v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
                 :key="dict.value"
                 :label="dict.label"
-                :value="dict.value"
-              />
+                :value="dict.value" />
             </el-select>
           </el-form-item>
 
@@ -52,13 +49,49 @@
 
       <!-- 列表 -->
       <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
+        <div class="stats-cards">
+          <div class="stats-card stats-card--expired">
+            <div class="stats-card__header">
+              <el-icon class="stats-card__icon" :size="28">
+                <Icon icon="ep:info-filled" />
+              </el-icon>
+              <div class="stats-card__label">已过期</div>
+            </div>
+            <div class="stats-card__value text-[40px]! pt-10 text-center!">
+              <CountTo
+                :duration="2600"
+                :end-val="expired"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!"
+            /></div>
+          </div>
+          <div class="stats-card stats-card--warn">
+            <div class="stats-card__header">
+              <el-icon class="stats-card__icon" :size="28">
+                <Icon icon="ep:bell-filled" />
+              </el-icon>
+              <div class="stats-card__label">90天预警</div>
+            </div>
+            <div class="stats-card__value text-[40px]! pt-10 text-center!">
+              <CountTo
+                :duration="2600"
+                :end-val="warn"
+                :start-val="0"
+                class="stats-card__value text-[40px]! pt-10 text-center! text-[#df8a28]!" />
+            </div>
+          </div>
+          <div class="stats-chart-card">
+            <div class="stats-card__label">分类统计</div>
+            <div ref="staticChartRef" class="stats-chart"></div>
+          </div>
+        </div>
         <zm-table
           :loading="loading"
           :data="list"
-          height="calc(78vh - 150px)"
+          height="calc(49vh - 150px)"
           :show-overflow-tooltip="true"
           :row-style="tableRowStyle"
-        >
+          :row-class-name="tableRowClassName">
           <zm-table-column :label="t('monitor.serial')" width="70" align="center" fixed="left">
             <template #default="scope">
               {{ scope.$index + 1 }}
@@ -66,6 +99,7 @@
           </zm-table-column>
           <zm-table-column label="名称" align="center" prop="measureName" fixed="left" />
           <zm-table-column label="编码" align="center" prop="measureCode" fixed="left" />
+          <zm-table-column label="部门名称" align="center" prop="deptName" />
           <zm-table-column label="计量单位" align="center" prop="measureUnit" />
 
           <zm-table-column label="责任人" align="center" prop="dutyPerson" />
@@ -102,8 +136,7 @@
             align="center"
             fixed="right"
             action
-            min-width="120px"
-          >
+            min-width="120px">
             <template #default="scope">
               <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
               <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
@@ -115,8 +148,7 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+          @pagination="getList" />
       </ContentWrap>
       <ContentWrap style="margin-top: -5px">
         <el-alert title="台账已过期红色预警" type="error" show-icon :closable="false">
@@ -124,6 +156,17 @@
             <Bell />
           </template>
         </el-alert>
+
+        <el-alert
+          title="台账90天橙色预警"
+          type="warning"
+          show-icon
+          :closable="false"
+          style="margin-top: 5px">
+          <template #icon>
+            <Bell />
+          </template>
+        </el-alert>
       </ContentWrap>
     </el-col>
   </el-row>
@@ -134,15 +177,13 @@
     v-model="dialogVisible"
     width="800px"
     destroy-on-close
-    @close="closeDialog"
-  >
+    @close="closeDialog">
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
       label-width="120px"
-      v-loading="formLoading"
-    >
+      v-loading="formLoading">
       <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="计量器具名称" prop="measureName">
@@ -167,8 +208,7 @@
               check-strictly
               node-key="id"
               filterable
-              placeholder="请选择所在部门"
-            />
+              placeholder="请选择所在部门" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -198,14 +238,12 @@
               v-model="formData.classify"
               placeholder="请选择分类"
               clearable
-              class="!w-240px"
-            >
+              class="!w-240px">
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.MEASURE_TYPE)"
                 :key="dict.value"
                 :label="dict.label"
-                :value="dict.value"
-              />
+                :value="dict.value" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -216,8 +254,7 @@
               :precision="2"
               :step="1"
               placeholder="请输入价格"
-              style="width: 100%"
-            />
+              style="width: 100%" />
           </el-form-item>
         </el-col>
       </el-row>
@@ -230,8 +267,7 @@
               type="date"
               value-format="x"
               placeholder="请选择采购日期"
-              style="width: 100%"
-            />
+              style="width: 100%" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -280,16 +316,16 @@
     title="检测信息"
     size="60%"
     direction="rtl"
-    :before-close="closeDrawer"
-  >
+    :before-close="closeDrawer">
     <div v-loading="drawerLoading">
       <zm-table
         :loading="loading"
         :data="measureDetectionData"
         :stripe="true"
+        :row-style="tableRowStyle"
+        :row-class-name="tableRowClassName"
         height="80vh"
-        :show-overflow-tooltip="true"
-      >
+        :show-overflow-tooltip="true">
         <zm-table-column :label="t('monitor.serial')" width="70" align="center" fixed="left">
           <template #default="scope">
             {{ scope.$index + 1 }}
@@ -300,8 +336,7 @@
           align="center"
           prop="measureName"
           min-width="160"
-          fixed="left"
-        />
+          fixed="left" />
         <zm-table-column label="证书编码" align="center" prop="measureCertNo" min-width="160" />
         <zm-table-column label="检测/校准日期" align="center" prop="detectDate" width="140">
           <template #default="scope">
@@ -313,8 +348,7 @@
           label="检测/校准标准"
           align="center"
           prop="detectStandard"
-          min-width="160"
-        />
+          min-width="160" />
         <zm-table-column label="检测/校准内容" align="center" prop="detectContent" min-width="220">
           <template #default="scope">
             <div class="detect-content" v-html="scope.row.detectContent"></div>
@@ -333,8 +367,7 @@
           prop="file"
           min-width="90"
           fixed="right"
-          action
-        >
+          action>
           <template #default="scope">
             <el-button v-if="scope.row.file" link type="primary" @click="viewFile(scope.row.file)">
               查看
@@ -349,8 +382,7 @@
           :total="totalMsg"
           v-model:page="queryParams2.pageNo"
           v-model:limit="queryParams2.pageSize"
-          @pagination="getList2"
-        />
+          @pagination="getList2" />
       </div>
     </div>
   </el-drawer>
@@ -359,8 +391,7 @@
     <div
       v-for="(file, index) in fileList"
       :key="index"
-      class="flex items-center justify-between mt-5"
-    >
+      class="flex items-center justify-between mt-5">
       <span class="file-name-text">{{ extractFileName(file) }}</span>
       <div>
         <el-button link type="primary" @click="viewFileInfo(file)">
@@ -381,6 +412,7 @@
 </template>
 
 <script setup lang="ts">
+import * as echarts from 'echarts'
 import { IotInstrumentApi, IotMeasureDetectApi } from '@/api/pms/qhse/index'
 import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { handleTree } from '@/utils/tree'
@@ -390,10 +422,13 @@ import { ElMessageBox } from 'element-plus'
 const deptList = ref<Tree[]>([]) // 树形结构
 const deptList2 = ref<Tree[]>([]) // 树形结构
 import { formatDate } from '@/utils/formatTime'
+import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 
+const userStore = useUserStore()
+
 defineOptions({ name: 'IotQHSEMeasure' })
 
 const loading = ref(true) // 列表的加载中
@@ -405,8 +440,14 @@ const isLeftContentCollapsed = ref(false)
 
 const { t } = useI18n()
 
+type StaticItem = {
+  classify: string
+  count: number
+}
+
 const list = ref([]) // 列表的数据
 const total = ref(0) // 列表的总页数
+const staticChartRef = ref<HTMLDivElement>()
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -420,9 +461,22 @@ const tableRowStyle = ({ row }) => {
   if (row.expired) {
     return { backgroundColor: '#ffe6e6' }
   }
+  if (row.alertWarn) {
+    return { backgroundColor: '#e19f1a' }
+  }
   return {}
 }
 
+const tableRowClassName = ({ row }) => {
+  if (row.expired) {
+    return 'expired-row'
+  }
+  if (row.alertWarn) {
+    return 'alert-warn-row'
+  }
+  return ''
+}
+
 // 对话框相关
 const dialogVisible = ref(false)
 const dialogTitle = ref('')
@@ -535,6 +589,7 @@ const handleExport = async () => {
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   await getList()
+  await getStatic()
 }
 
 /** 搜索按钮操作 */
@@ -766,19 +821,309 @@ const handleDownload = async (url) => {
   }
 }
 
+let staticData = ref<StaticItem[]>([])
+let expired = ref(0)
+let warn = ref(0)
+let staticChart: echarts.ECharts | null = null
+
+function getStaticChartOption(): echarts.EChartsOption {
+  return {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    grid: {
+      left: 16,
+      right: 16,
+      top: 24,
+      bottom: 16,
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: staticData.value.map((item) => item.classify),
+      axisTick: {
+        show: false
+      },
+      axisLine: {
+        lineStyle: {
+          color: '#d9e2ef'
+        }
+      },
+      axisLabel: {
+        color: '#6b7280',
+        fontSize: 12,
+        interval: 0
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#94a3b8',
+        fontSize: 12
+      },
+      splitLine: {
+        lineStyle: {
+          color: '#edf2f7',
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        type: 'bar',
+        barWidth: 32,
+        data: staticData.value.map((item) => item.count),
+        itemStyle: {
+          borderRadius: [8, 8, 0, 0],
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#79b8ff' },
+            { offset: 1, color: '#2f78f6' }
+          ])
+        },
+        label: {
+          show: true,
+          position: 'top',
+          color: '#2563eb',
+          fontSize: 12,
+          fontWeight: 700
+        }
+      }
+    ]
+  }
+}
+
+function renderStaticChart() {
+  if (!staticChartRef.value) return
+  if (!staticChart) {
+    staticChart = echarts.init(staticChartRef.value)
+  }
+  staticChart.setOption(getStaticChartOption(), true)
+}
+
+function resizeStaticChart() {
+  staticChart?.resize()
+}
+
+function destroyStaticChart() {
+  if (staticChart) {
+    staticChart.dispose()
+    staticChart = null
+  }
+}
+
+async function getStatic() {
+  if (queryParams.deptId) {
+    const res = await IotInstrumentApi.getInstrumentStatistics(queryParams.deptId)
+    staticData.value = res.classify
+    expired.value = res.expired
+    warn.value = res.warn
+  } else {
+    const res = await IotInstrumentApi.getInstrumentStatistics(userStore.user.deptId)
+    staticData.value = res.classify
+    expired.value = res.expired
+    warn.value = res.warn
+  }
+  nextTick(() => {
+    renderStaticChart()
+  })
+}
+
 onMounted(async () => {
   getList()
+  getStatic()
 
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+  window.addEventListener('resize', resizeStaticChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeStaticChart)
+  destroyStaticChart()
 })
 </script>
 
 <style scoped>
-::deep(.el-tree--highlight-current) {
+:deep(.el-tree--highlight-current) {
   height: 200px !important;
 }
-::deep(.el-transfer-panel__body) {
+:deep(.el-transfer-panel__body) {
   height: 700px !important;
 }
+
+.stats-cards {
+  display: grid;
+  grid-template-columns: 180px 180px minmax(0, 1fr);
+  gap: 12px;
+  margin-bottom: 16px;
+  align-items: stretch;
+}
+
+.stats-card {
+  padding: 14px 16px;
+  background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
+  border: 1px solid #e4ecf7;
+  border-radius: 10px;
+  box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
+}
+
+.stats-card__label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+.stats-card__value {
+  margin-top: 8px;
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1;
+  color: #1f5bb8;
+}
+
+.stats-card--expired {
+  background: linear-gradient(180deg, #fff4f4 0%, #ffe8e8 100%);
+  border-color: #ffcfcf;
+}
+
+.stats-card--expired .stats-card__value {
+  color: #de3b3b;
+}
+
+.stats-card--warn {
+  background: linear-gradient(180deg, #fff8ef 0%, #ffeed9 100%);
+  border-color: #ffd7a1;
+}
+
+.stats-card--warn .stats-card__value {
+  color: #d97706;
+}
+
+.stats-chart-card {
+  padding: 14px 16px 10px;
+  background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
+  border: 1px solid #e4ecf7;
+  border-radius: 10px;
+  box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
+}
+
+.stats-chart {
+  height: 130px;
+  margin-top: 6px;
+}
+
+.stats-card {
+  padding: 14px 16px;
+  background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
+  border: 1px solid #e4ecf7;
+  border-radius: 10px;
+  box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
+}
+
+.stats-card__header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.stats-card__icon {
+  color: inherit;
+  opacity: 0.85;
+}
+
+.stats-card__label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+.stats-card__value {
+  margin-top: 8px;
+  font-size: 28px;
+  font-weight: 700;
+  line-height: 1;
+  color: #1f5bb8;
+}
+
+.stats-card__desc {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #9ca3af;
+  font-weight: 500;
+}
+
+.stats-card--expired {
+  background: linear-gradient(180deg, #fff4f4 0%, #ffe8e8 100%);
+  border-color: #ffcfcf;
+}
+
+.stats-card--expired .stats-card__value {
+  color: #de3b3b;
+}
+
+.stats-card--expired .stats-card__icon {
+  color: #de3b3b;
+}
+
+.stats-card--warn {
+  background: linear-gradient(180deg, #fff8ef 0%, #ffeed9 100%);
+  border-color: #ffd7a1;
+}
+
+.stats-card--warn .stats-card__value {
+  color: #d97706;
+}
+
+.stats-card--warn .stats-card__icon {
+  color: #d97706;
+}
+
+/* 过期行的红色背景 - 基础状态 */
+:deep(.el-table__body tr.expired-row > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+/* 过期行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.expired-row:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 确保斑马纹不影响过期行 */
+:deep(.el-table__body tr.expired-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+:deep(.el-table__body tr.expired-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 预警行的橙色背景 - 基础状态 */
+:deep(.el-table__body tr.alert-warn-row > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+/* 预警行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.alert-warn-row:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
+
+/* 确保斑马纹不影响预警行 */
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
 </style>

+ 950 - 0
src/views/pms/qhse/kanban/index.vue

@@ -0,0 +1,950 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  AlarmClock,
+  Checked,
+  CollectionTag,
+  DataAnalysis,
+  DocumentChecked,
+  Files,
+  Flag,
+  Histogram,
+  Opportunity,
+  Postcard,
+  Warning
+} from '@element-plus/icons-vue'
+import {
+  ANIMATION,
+  CHART_RENDERER,
+  DESIGN_HEIGHT,
+  DESIGN_WIDTH,
+  FONT_FAMILY,
+  THEME,
+  createTooltip
+} from '@/utils/kb'
+
+defineOptions({
+  name: 'PmsQhseKanban'
+})
+
+type SummaryCard = {
+  title: string
+  value: string
+  note: string
+  accent: string
+  glow: string
+  icon: any
+}
+
+type MetricBar = {
+  label: string
+  value: number
+  color: string
+}
+
+type RiskZone = {
+  title: string
+  desc: string
+  color: string
+}
+
+type PermitStat = {
+  label: string
+  value: number
+  color: string
+}
+
+type BottomCard = {
+  title: string
+  icon: any
+  accent: string
+  glow: string
+  lines: string[]
+}
+
+const wrapperRef = ref<HTMLDivElement>()
+const hazardChartRef = ref<HTMLDivElement>()
+const socChartRef = ref<HTMLDivElement>()
+const scale = ref(1)
+const supportsZoom = ref(false)
+
+let resizeObserver: ResizeObserver | null = null
+let resizeRaf = 0
+let hazardChart: echarts.ECharts | null = null
+let socChart: echarts.ECharts | null = null
+
+const pageTitle = 'QHSE管理看板'
+
+const summaryCards: SummaryCard[] = [
+  {
+    title: '风险总数(处)',
+    value: '126',
+    note: 'R:12',
+    accent: '#ff5a62',
+    glow: 'rgba(255, 90, 98, 0.26)',
+    icon: Warning
+  },
+  {
+    title: '本月隐患(条)',
+    value: '29',
+    note: 'Exp:2',
+    accent: '#ff9f2f',
+    glow: 'rgba(255, 159, 47, 0.24)',
+    icon: Opportunity
+  },
+  {
+    title: '隐患整改率',
+    value: '92.3%',
+    note: '',
+    accent: '#2ac7c9',
+    glow: 'rgba(42, 199, 201, 0.26)',
+    icon: DataAnalysis
+  },
+  {
+    title: '本月作业许可',
+    value: '11',
+    note: '',
+    accent: '#4f8dff',
+    glow: 'rgba(79, 141, 255, 0.22)',
+    icon: DocumentChecked
+  },
+  {
+    title: '人员持证率',
+    value: '97.8%',
+    note: 'Warn:3',
+    accent: '#f2b800',
+    glow: 'rgba(242, 184, 0, 0.22)',
+    icon: Postcard
+  }
+]
+
+const hazardBars: MetricBar[] = [
+  { label: '总数', value: 657, color: '#4f8dff' },
+  { label: '已整改', value: 628, color: '#43c7ca' },
+  { label: '未整改', value: 29, color: '#ff981f' }
+]
+
+const incidentStats = [
+  { label: '安全事故', value: '0起', accent: '#2ac7c9' },
+  { label: '安全生产天数', value: '3起', accent: '#f2c11a' }
+]
+
+const riskZones: RiskZone[] = [
+  { title: '高危风险区', desc: '危化库 / 试压区 / 配电房', color: '#ff4c49' },
+  { title: '中风险区', desc: '焊接 / 机加 / 吊装区', color: '#ff981f' },
+  { title: '低风险区', desc: '物料库 / 维修 / 装卸区', color: '#f2c11a' },
+  { title: '安全控制区', desc: '办公区 / 展厅 / 主通道', color: '#5794ff' }
+]
+
+const permitStats: PermitStat[] = [
+  { label: '个人防护', value: 18, color: '#4f8dff' },
+  { label: '规范操作', value: 26, color: '#43c7ca' },
+  { label: '规范指挥', value: 12, color: '#ffb14a' },
+  { label: '人员位置', value: 9, color: '#ff7a7a' },
+  { label: '作业场所', value: 15, color: '#8d8cff' }
+]
+
+const qualificationWarnings = [
+  { label: '证件过期', value: '0人', accent: '#24364f' },
+  { label: '即将到期', value: '3人(需复审)', accent: '#e6ab00' }
+]
+
+const bottomCards: BottomCard[] = [
+  {
+    title: '体系合规',
+    icon: Files,
+    accent: '#39c6cc',
+    glow: 'rgba(57, 198, 204, 0.22)',
+    lines: ['内审已完成', '外审待安排']
+  },
+  {
+    title: '安全检测',
+    icon: Histogram,
+    accent: '#4f8dff',
+    glow: 'rgba(79, 141, 255, 0.2)',
+    lines: ['在用: 32台', '待检: 2台(重点关注)']
+  },
+  {
+    title: '应急演练',
+    icon: AlarmClock,
+    accent: '#ff5b61',
+    glow: 'rgba(255, 91, 97, 0.22)',
+    lines: ['年度应急演练:80次']
+  },
+  {
+    title: '质量检验',
+    icon: Checked,
+    accent: '#f2c11a',
+    glow: 'rgba(242, 193, 26, 0.2)',
+    lines: ['产品合格率', '98.7%(达标)']
+  },
+  {
+    title: '环境危废',
+    icon: Flag,
+    accent: '#28c98b',
+    glow: 'rgba(40, 201, 139, 0.2)',
+    lines: ['危险暂存合规', '三废处置100%达标']
+  }
+]
+
+const targetWrapperStyle = computed(() => ({
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const targetAreaStyle = computed(() => {
+  const style = {
+    width: `${DESIGN_WIDTH}px`,
+    height: `${DESIGN_HEIGHT}px`,
+    transformOrigin: '0 0'
+  } as Record<string, string | number>
+
+  if (supportsZoom.value) {
+    style.zoom = scale.value
+  } else {
+    style.transform = `scale(${scale.value})`
+  }
+
+  return style
+})
+
+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)
+  })
+}
+
+onMounted(() => {
+  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  initHazardChart()
+  initSocChart()
+  window.addEventListener('resize', updateScale)
+  window.addEventListener('resize', resizeHazardChart)
+  window.addEventListener('resize', resizeSocChart)
+})
+
+onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  window.removeEventListener('resize', resizeHazardChart)
+  window.removeEventListener('resize', resizeSocChart)
+  cancelAnimationFrame(resizeRaf)
+  destroyHazardChart()
+  destroySocChart()
+})
+
+function getHazardChartOption(): echarts.EChartsOption {
+  return {
+    ...ANIMATION,
+    grid: {
+      left: 20,
+      right: 10,
+      top: 24,
+      bottom: 24,
+      containLabel: true
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: 'rgba(31, 91, 184, 0.08)'
+        }
+      },
+      formatter(params: any) {
+        const item = Array.isArray(params) ? params[0] : params
+        return `${item.name}<br/>数量:${item.value}`
+      }
+    }),
+    xAxis: {
+      type: 'category',
+      data: hazardBars.map((item) => item.label),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#5b6f8f',
+        fontSize: 16,
+        fontWeight: 700,
+        fontFamily: FONT_FAMILY,
+        interval: 0
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#8a9bb5',
+        fontSize: 13,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        type: 'bar',
+        data: hazardBars.map((item) => ({
+          value: item.value,
+          itemStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: item.color },
+              { offset: 1, color: `${item.color}99` }
+            ]),
+            shadowBlur: 14,
+            shadowColor: item.color,
+            borderRadius: [0, 0, 0, 0]
+          }
+        })),
+        barWidth: 70,
+
+        backgroundStyle: {
+          color: 'rgba(31, 91, 184, 0.06)',
+          borderRadius: 0
+        },
+        label: {
+          show: true,
+          position: 'top',
+          color: '#3c5f96',
+          fontSize: 16,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return `${params.value}`
+          }
+        },
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 18
+          }
+        }
+      }
+    ]
+  }
+}
+
+function initHazardChart() {
+  if (!hazardChartRef.value) return
+  if (hazardChart) {
+    hazardChart.dispose()
+  }
+  hazardChart = echarts.init(hazardChartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  hazardChart.setOption(getHazardChartOption(), true)
+}
+
+function resizeHazardChart() {
+  hazardChart?.resize()
+}
+
+function destroyHazardChart() {
+  if (hazardChart) {
+    hazardChart.dispose()
+    hazardChart = null
+  }
+}
+
+function getSocChartOption(): echarts.EChartsOption {
+  return {
+    ...ANIMATION,
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `${params.name}<br/>数量:${params.value}<br/>占比:${params.percent}%`
+      }
+    }),
+    legend: {
+      orient: 'vertical',
+      right: 4,
+      top: 'center',
+      itemWidth: 12,
+      itemHeight: 12,
+      icon: 'circle',
+      textStyle: {
+        color: '#5b6f8f',
+        fontSize: 15,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    series: [
+      {
+        name: 'SOC卡类型',
+        type: 'pie',
+        radius: ['50%', '74%'],
+        center: ['34%', '54%'],
+        avoidLabelOverlap: true,
+        itemStyle: {
+          borderColor: 'rgba(255, 255, 255, 0.85)',
+          borderWidth: 2,
+          shadowBlur: 10,
+          shadowColor: 'rgba(31, 91, 184, 0.12)'
+        },
+        label: {
+          show: true,
+          color: '#4e6483',
+          fontSize: 13,
+          fontWeight: 700,
+          formatter: '{d}%'
+        },
+        labelLine: {
+          lineStyle: {
+            color: 'rgba(91, 111, 143, 0.5)'
+          },
+          length: 10,
+          length2: 8
+        },
+        data: permitStats.map((item) => ({
+          name: item.label,
+          value: item.value,
+          itemStyle: {
+            color: item.color
+          }
+        })),
+        emphasis: {
+          scale: true,
+          scaleSize: 6
+        }
+      }
+    ],
+    graphic: [
+      {
+        type: 'text',
+        left: '28.8%',
+        top: '43%',
+        style: {
+          text: 'SOC卡',
+
+          fill: '#6b7f9c',
+          fontSize: 18,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      {
+        type: 'text',
+        left: '26.5%',
+        top: '50%',
+        textAlign: 'center',
+        style: {
+          text: `${permitStats.reduce((sum, item) => sum + item.value, 0)}`,
+
+          fill: '#114a9b',
+          fontSize: 30,
+          fontWeight: 700,
+          fontFamily: 'YouSheBiaoTiHei, sans-serif'
+        }
+      }
+    ]
+  }
+}
+
+function initSocChart() {
+  if (!socChartRef.value) return
+  if (socChart) {
+    socChart.dispose()
+  }
+  socChart = echarts.init(socChartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  socChart.setOption(getSocChartOption(), true)
+}
+
+function resizeSocChart() {
+  socChart?.resize()
+}
+
+function destroySocChart() {
+  if (socChart) {
+    socChart.dispose()
+    socChart = null
+  }
+}
+</script>
+
+<template>
+  <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div id="qhse-kanban" class="bg qhse-board" :style="targetAreaStyle">
+        <header class="header">{{ pageTitle }}</header>
+        <div class="board-body">
+          <section class="panel summary-panel kb-stage-card kb-stage-card--1">
+            <div class="panel-title">
+              <span class="icon-decorator"><span></span><span></span></span>
+              风险总览
+            </div>
+            <div class="summary-grid">
+              <article
+                v-for="card in summaryCards"
+                :key="card.title"
+                class="summary-card summary-tile"
+                :style="
+                  {
+                    '--card-accent': card.accent,
+                    '--card-glow': card.glow
+                  } as any
+                ">
+                <span class="summary-card__shine"></span>
+                <span class="summary-card__corner"></span>
+                <div class="summary-card__icon">
+                  <el-icon class="summary-card__icon-glyph">
+                    <component :is="card.icon" />
+                  </el-icon>
+                </div>
+                <div class="summary-card__content">
+                  <div class="summary-card__label">{{ card.title }}</div>
+                  <div class="summary-tile__meta">
+                    <span class="summary-tile__value">{{ card.value }}</span>
+                    <span class="summary-tile__note" :style="{ color: card.accent }">{{
+                      card.note
+                    }}</span>
+                  </div>
+                </div>
+              </article>
+            </div>
+          </section>
+
+          <div class="board-main">
+            <div class="left-column">
+              <section class="panel board-panel kb-stage-card kb-stage-card--2">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  隐患排查治理统计
+                </div>
+                <div ref="hazardChartRef" class="chart-panel chart-panel--echart"></div>
+              </section>
+
+              <section class="panel board-panel kb-stage-card kb-stage-card--3">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  事故事件趋势(近12月)
+                </div>
+                <div class="incident-panel">
+                  <div class="incident-graphic">
+                    <div class="incident-graphic__axis"></div>
+                    <div class="incident-graphic__area"></div>
+                    <div class="incident-graphic__line"></div>
+                  </div>
+                  <div class="incident-metrics">
+                    <div v-for="item in incidentStats" :key="item.label" class="incident-metric">
+                      <span :style="{ color: item.label === '安全生产天数' ? '#259745' : '' }"
+                        >{{ item.label }}:</span
+                      >
+                      <strong :style="{ color: item.accent }">{{ item.value }}</strong>
+                    </div>
+                  </div>
+                </div>
+              </section>
+            </div>
+
+            <div class="center-column">
+              <section class="panel board-panel board-panel--center kb-stage-card kb-stage-card--4">
+                <div class="panel-title panel-title--center">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  安全风险四色动态分布
+                </div>
+                <div class="risk-grid">
+                  <article v-for="zone in riskZones" :key="zone.title" class="risk-card">
+                    <div class="risk-card__title">
+                      <span class="risk-card__dot" :style="{ background: zone.color }"></span>
+                      <span :style="{ color: zone.color }">{{ zone.title }}</span>
+                    </div>
+                    <div class="risk-card__desc">{{ zone.desc }}</div>
+                  </article>
+                </div>
+              </section>
+            </div>
+
+            <div class="right-column">
+              <section class="panel board-panel kb-stage-card kb-stage-card--5">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  SOC卡类型
+                </div>
+                <div ref="socChartRef" class="soc-chart-panel"></div>
+              </section>
+
+              <section class="panel board-panel kb-stage-card kb-stage-card--6">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  人员资质风险预警
+                </div>
+                <div class="qualification-panel">
+                  <div class="qualification-icon">
+                    <el-icon>
+                      <CollectionTag />
+                    </el-icon>
+                  </div>
+                  <div class="qualification-list">
+                    <div
+                      v-for="item in qualificationWarnings"
+                      :key="item.label"
+                      class="qualification-item">
+                      <span class="qualification-item__label">{{ item.label }}:</span>
+                      <strong :style="{ color: item.accent }">{{ item.value }}</strong>
+                    </div>
+                  </div>
+                </div>
+              </section>
+            </div>
+          </div>
+
+          <section class="bottom-grid">
+            <article
+              v-for="(card, index) in bottomCards"
+              :key="card.title"
+              class="panel bottom-card kb-stage-card"
+              :class="`kb-stage-card--${index + 7}`"
+              :style="
+                {
+                  '--card-accent': card.accent,
+                  '--card-glow': card.glow
+                } as any
+              ">
+              <span class="summary-card__shine"></span>
+              <div class="bottom-card__header">
+                <div class="summary-card__icon bottom-card__icon">
+                  <el-icon class="summary-card__icon-glyph">
+                    <component :is="card.icon" />
+                  </el-icon>
+                </div>
+                <div class="bottom-card__title">{{ card.title }}</div>
+              </div>
+              <div class="bottom-card__content">
+                <p v-for="line in card.lines" :key="line">{{ line }}</p>
+              </div>
+            </article>
+          </section>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.qhse-board {
+  color: #24364f;
+}
+
+.board-body {
+  padding: 18px 18px 24px;
+}
+
+.summary-panel {
+  padding-bottom: 22px;
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 24px;
+  margin-top: 20px;
+}
+
+.summary-tile {
+  display: flex;
+  height: 136px;
+  padding: 28px 28px 24px;
+  border-radius: 22px;
+  align-items: center;
+  gap: 22px;
+}
+
+.summary-card__content {
+  position: relative;
+  z-index: 2;
+  min-width: 0;
+  flex: 1;
+}
+
+.summary-tile__meta {
+  display: flex;
+  margin-top: 14px;
+  align-items: baseline;
+  gap: 8px;
+}
+
+.summary-tile__value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: 40px;
+  line-height: 1;
+  color: #114a9b;
+  letter-spacing: 1px;
+}
+
+.summary-tile__note {
+  font-size: 18px;
+  font-weight: 700;
+}
+
+.board-main {
+  display: grid;
+  grid-template-columns: 1.02fr 1fr 0.98fr;
+  gap: 28px;
+  margin-top: 24px;
+}
+
+.left-column,
+.right-column {
+  display: grid;
+  gap: 24px;
+}
+
+.center-column {
+  display: block;
+}
+
+.board-panel {
+  min-height: 258px;
+}
+
+.board-panel--center {
+  min-height: 540px;
+}
+
+.chart-panel {
+  margin-top: 18px;
+}
+
+.chart-panel--bars {
+  height: 176px;
+}
+
+.chart-panel--echart {
+  height: 188px;
+}
+
+.incident-panel {
+  display: grid;
+  margin-top: 50px;
+  grid-template-columns: 180px 1fr;
+  align-items: center;
+  gap: 28px;
+}
+
+.incident-graphic {
+  position: relative;
+  width: 170px;
+  height: 132px;
+}
+
+.incident-graphic__axis {
+  position: absolute;
+  inset: 24px 16px 16px 16px;
+  border-bottom: 6px solid #4f8dff;
+  border-left: 6px solid #4f8dff;
+  border-radius: 2px;
+  opacity: 0.92;
+}
+
+.incident-graphic__area {
+  position: absolute;
+  right: 22px;
+  bottom: 24px;
+  width: 102px;
+  height: 68px;
+  background: linear-gradient(180deg, rgb(79 141 255 / 88%) 0%, rgb(79 141 255 / 28%) 100%);
+  clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 100%);
+}
+
+.incident-graphic__line {
+  position: absolute;
+  right: 22px;
+  bottom: 24px;
+  width: 102px;
+  height: 68px;
+  border-top: 5px solid #4f8dff;
+  clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 5%, 54% 77%, 22% 55%, 0 100%);
+}
+
+.incident-metrics {
+  display: grid;
+  gap: 20px;
+}
+
+.incident-metric {
+  font-size: 24px;
+  font-weight: 700;
+  color: #556b89;
+}
+
+.incident-metric strong {
+  margin-left: 4px;
+  font-size: 34px;
+  line-height: 1;
+}
+
+.panel-title--center {
+  justify-content: center;
+  padding-left: 0;
+  font-size: 32px;
+}
+
+.panel-title--center .icon-decorator {
+  left: 28px;
+}
+
+.risk-grid {
+  display: grid;
+  padding: 30px;
+  margin-top: 34px;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 26px 28px;
+}
+
+.risk-card {
+  min-height: 182px;
+  padding: 34px 24px;
+  background: linear-gradient(180deg, rgb(255 255 255 / 42%) 0%, rgb(220 232 250 / 28%) 100%);
+  border: 1px solid rgb(255 255 255 / 58%);
+  border-radius: 18px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 74%),
+    0 8px 18px rgb(63 103 171 / 7%);
+}
+
+.risk-card__title {
+  display: flex;
+  font-size: 20px;
+  font-weight: 800;
+  align-items: center;
+  gap: 12px;
+}
+
+.risk-card__dot {
+  width: 18px;
+  height: 18px;
+  border-radius: 999px;
+  box-shadow: 0 0 0 6px rgb(255 255 255 / 35%);
+}
+
+.risk-card__desc {
+  margin-top: 36px;
+  font-size: 17px;
+  font-weight: 600;
+  color: #6f819a;
+  line-height: 1.55;
+}
+
+.soc-chart-panel {
+  height: 220px;
+  // margin-top: 10px;
+}
+
+.qualification-panel {
+  display: grid;
+  margin-top: 26px;
+  padding: 28px;
+  grid-template-columns: 140px 1fr;
+  align-items: center;
+  gap: 26px;
+}
+
+.qualification-icon {
+  display: flex;
+  width: 132px;
+  height: 132px;
+  font-size: 74px;
+  color: #f09717;
+  background: radial-gradient(
+    circle at 50% 40%,
+    rgb(255 255 255 / 74%) 0%,
+    rgb(231 240 255 / 42%) 100%
+  );
+  border: 1px solid rgb(255 255 255 / 72%);
+  border-radius: 32px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 85%),
+    0 10px 22px rgb(55 94 160 / 10%);
+  align-items: center;
+  justify-content: center;
+}
+
+.qualification-list {
+  display: grid;
+  gap: 22px;
+}
+
+.qualification-item {
+  font-size: 24px;
+  font-weight: 700;
+  color: #516785;
+}
+
+.qualification-item strong {
+  font-size: 28px;
+}
+
+.bottom-grid {
+  display: grid;
+  margin-top: 26px;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 24px;
+}
+
+.bottom-card {
+  min-height: 190px;
+  padding: 28px 22px 24px;
+  overflow: hidden;
+}
+
+.bottom-card__header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.bottom-card__icon {
+  width: 54px;
+  height: 54px;
+}
+
+.bottom-card__title {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: 24px;
+  color: #114a9b;
+  letter-spacing: 1px;
+}
+
+.bottom-card__content {
+  position: relative;
+  z-index: 2;
+  margin-top: 24px;
+}
+
+.bottom-card__content p {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 700;
+  color: #5d718e;
+  line-height: 1.7;
+}
+</style>

+ 23 - 29
src/views/system/loginlog/index.vue

@@ -8,16 +8,14 @@
       :model="queryParams"
       ref="queryFormRef"
       :inline="true"
-      label-width="68px"
-    >
+      label-width="68px">
       <el-form-item label="用户名称" prop="username">
         <el-input
           v-model="queryParams.username"
           placeholder="请输入用户名称"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="登录地址" prop="userIp">
         <el-input
@@ -25,8 +23,7 @@
           placeholder="请输入登录地址"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item label="登录日期" prop="createTime">
         <el-date-picker
@@ -36,8 +33,7 @@
           start-placeholder="开始日期"
           end-placeholder="结束日期"
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-        />
+          class="!w-240px" />
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
@@ -47,8 +43,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['system:login-log:export']"
-        >
+          v-hasPermi="['system:login-log:export']">
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
       </el-form-item>
@@ -57,48 +52,47 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="日志编号" align="center" prop="id" />
-      <el-table-column label="操作类型" align="center" prop="logType">
+    <zm-table :loading="loading" :data="list">
+      <zm-table-column label="日志编号" align="center" prop="id" />
+      <zm-table-column label="操作类型" align="center" prop="logType" min-width="100">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="scope.row.logType" />
         </template>
-      </el-table-column>
-      <el-table-column label="用户名称" align="center" prop="username" width="180" />
-      <el-table-column label="登录地址" align="center" prop="userIp" width="180" />
-      <el-table-column label="浏览器" align="center" prop="userAgent" />
-      <el-table-column label="登陆结果" align="center" prop="result">
+      </zm-table-column>
+      <zm-table-column label="用户名称" align="center" prop="username" />
+      <zm-table-column label="姓名" align="center" prop="nickname" />
+      <zm-table-column label="所在部门" align="center" prop="deptName" />
+      <zm-table-column label="登录地址" align="center" prop="userIp" />
+      <zm-table-column label="浏览器" align="center" prop="userAgent" />
+      <zm-table-column label="登陆结果" align="center" prop="result">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="scope.row.result" />
         </template>
-      </el-table-column>
-      <el-table-column
+      </zm-table-column>
+      <zm-table-column
         label="登录日期"
         align="center"
         prop="createTime"
         width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column label="操作" align="center">
+        :formatter="dateFormatter" />
+      <zm-table-column label="操作" align="center" fixed="right" action>
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openDetail(scope.row)"
-            v-hasPermi="['system:login-log:query']"
-          >
+            v-hasPermi="['system:login-log:query']">
             详情
           </el-button>
         </template>
-      </el-table-column>
-    </el-table>
+      </zm-table-column>
+    </zm-table>
     <!-- 分页 -->
     <Pagination
       :total="total"
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      @pagination="getList" />
   </ContentWrap>
 
   <!-- 表单弹窗:详情 -->