فهرست منبع

修复 页面显示

bzd_lxf 1 ماه پیش
والد
کامیت
d438615b27

+ 6 - 0
pm-admin/src/main/java/com/pm/web/controller/accessControl/AccessControlController.java

@@ -115,6 +115,7 @@ public class AccessControlController  extends BaseController {
         PageData pd = this.getPageData();
         String location = pd.getString("location");
         String deviceStatus = pd.getString("deviceStatus");
+        String doorStatus = pd.getString("doorStatus");
         // 条件过滤
         if (location != null && !location.isEmpty()) {
             data = data.stream()
@@ -126,6 +127,11 @@ public class AccessControlController  extends BaseController {
                     .filter(item -> item.get("deviceStatus").toString().equalsIgnoreCase(deviceStatus))
                     .collect(Collectors.toList());
         }
+        if (doorStatus != null && !doorStatus.isEmpty()) {
+            data = data.stream()
+                    .filter(item -> item.get("doorStatus").toString().equalsIgnoreCase(doorStatus))
+                    .collect(Collectors.toList());
+        }
 
         // 分页逻辑
         int total = data.size();

+ 349 - 29
pm-admin/src/main/java/com/pm/web/controller/buildingEquipmentMonitoring/BuildingEquipmentMonitoringController.java

@@ -3,15 +3,16 @@ package com.pm.web.controller.buildingEquipmentMonitoring;
 import com.pm.common.config.PageData;
 import com.pm.common.core.controller.BaseController;
 import com.pm.interfaceInfo.service.InterfaceInfoService;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.*;
 import java.util.stream.Collectors;
 
 
@@ -836,35 +837,37 @@ public class BuildingEquipmentMonitoringController extends BaseController {
     public Map<String, Object> getGenerateAreaData() throws Exception {
         List<Map<String, Object>> data = generateAreaData();
         PageData pd = this.getPageData();
-        String deviceName = pd.getString("deviceName");
+
+        // 过滤条件
+        String areaName = pd.getString("areaName");
+        String level = pd.getString("level");
         String deviceType = pd.getString("deviceType");
-        String floor = pd.getString("floor");
-        String area = pd.getString("area");
         String status = pd.getString("status");
-        // 条件过滤
-        if (deviceName != null && !deviceName.isEmpty()) {
-            data = data.stream()
-                    .filter(item -> item.get("deviceName").toString().contains(deviceName))
-                    .collect(Collectors.toList());
-        }
-        if (deviceType != null && !deviceType.isEmpty()) {
+
+        // 应用过滤
+        if (areaName != null && !areaName.isEmpty()) {
             data = data.stream()
-                    .filter(item -> item.get("deviceType").toString().contains(deviceType))
+                    .filter(item -> item.get("areaName").toString().contains(areaName))
                     .collect(Collectors.toList());
         }
-        if (floor != null && !floor.isEmpty()) {
+        if (level != null && !level.isEmpty()) {
             data = data.stream()
-                    .filter(item -> item.get("floor").toString().contains(floor))
+                    .filter(item -> item.get("level").toString().equals(level))
                     .collect(Collectors.toList());
         }
-        if (area != null && !area.isEmpty()) {
+        if (status != null && !status.isEmpty()) {
             data = data.stream()
-                    .filter(item -> item.get("area").toString().contains(area))
+                    .filter(item -> item.get("status").toString().equals(status))
                     .collect(Collectors.toList());
         }
-        if (status != null && !status.isEmpty()) {
+
+        // 根据设备类型和状态过滤
+        if (deviceType != null && !deviceType.isEmpty()) {
             data = data.stream()
-                    .filter(item -> item.get("status").toString().contains(status))
+                    .filter(area -> {
+                        List<Map<String, Object>> deviceStats = (List<Map<String, Object>>) area.get("deviceStats");
+                        return deviceStats.stream().anyMatch(stat -> stat.get("deviceType").equals(deviceType));
+                    })
                     .collect(Collectors.toList());
         }
 
@@ -874,14 +877,68 @@ public class BuildingEquipmentMonitoringController extends BaseController {
         int toIndex = Math.min(fromIndex + pd.getInteger("pageSize"), total);
         List<Map<String, Object>> pageData = data.subList(fromIndex, toIndex);
 
-        // 返回结果
-        Map<String, Object> response = Map.of(
+        return Map.of(
                 "msg", "操作成功",
                 "code", 200,
-                "data", Map.of("list", pageData,"total", total)
+                "data", Map.of("list", pageData, "total", total)
         );
-        return response;
     }
+    @GetMapping("/getAreaStats")
+    public Map<String, Object> getAreaStats() throws Exception {
+        List<Map<String, Object>> allAreas = generateAreaData();
+
+        // 计算统计数据
+        int totalAreas = allAreas.size();
+        int totalDevices = 0;
+        int onlineDevices = 0;
+        double totalHealthIndex = 0;
+
+        for (Map<String, Object> area : allAreas) {
+            List<Map<String, Object>> deviceStats = (List<Map<String, Object>>) area.get("deviceStats");
+            for (Map<String, Object> stat : deviceStats) {
+                // 安全地获取count值,处理可能的类型问题
+                Number countNum = (Number) stat.get("count");
+                int count = countNum != null ? countNum.intValue() : 0;
+
+                // 安全地获取onlineRate值
+                Number onlineRateNum = (Number) stat.get("onlineRate");
+                double onlineRate = onlineRateNum != null ? onlineRateNum.doubleValue() : 0.0;
+
+                totalDevices += count;
+                onlineDevices += (int) (count * onlineRate / 100);
+            }
+
+            String keyMetrics = (String) area.get("keyMetrics");
+            if (keyMetrics != null) {
+                String[] metrics = keyMetrics.split(",");
+                if (metrics.length >= 3) {
+                    try {
+                        totalHealthIndex += Double.parseDouble(metrics[2].trim());
+                    } catch (NumberFormatException e) {
+                        // 忽略解析错误
+                    }
+                }
+            }
+        }
+
+        double onlineRate = totalDevices > 0 ? (double) onlineDevices / totalDevices * 100 : 0;
+        double avgHealthIndex = totalAreas > 0 ? totalHealthIndex / totalAreas : 0;
+
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalAreas", totalAreas);
+        stats.put("totalDevices", totalDevices);
+        stats.put("onlineDevices", onlineDevices);
+        stats.put("onlineRate", Math.round(onlineRate * 10) / 10.0);
+        stats.put("avgHealthIndex", Math.round(avgHealthIndex));
+
+        return Map.of(
+                "msg", "操作成功",
+                "code", 200,
+                "data", stats
+        );
+    }
+
+
     @GetMapping("/generateCommandsData")
     public Map<String, Object> getGenerateCommandsData() throws Exception {
         List<Map<String, Object>> data = generateControlCommands();
@@ -891,6 +948,9 @@ public class BuildingEquipmentMonitoringController extends BaseController {
         String operatorId = pd.getString("operatorId");
         String feedbackStatus = pd.getString("feedbackStatus");
         String status = pd.getString("status");
+        String startTime = pd.getString("startTime");
+        String endTime = pd.getString("endTime");
+        data = filterByDateRange(data, startTime, endTime);
         // 条件过滤
         if (cmdId != null && !cmdId.isEmpty()) {
             data = data.stream()
@@ -932,4 +992,264 @@ public class BuildingEquipmentMonitoringController extends BaseController {
         );
         return response;
     }
+
+    // 1. 获取指令统计数据
+    @GetMapping("/getCommandStats")
+    public Map<String, Object> getCommandStats() throws Exception {
+        List<Map<String, Object>> allCommands = generateControlCommands();
+
+        // 计算统计数据
+        long todayTotal = allCommands.size(); // 实际应该过滤今天的数据
+        long successCount = allCommands.stream()
+                .filter(cmd -> "SUCCESS".equals(cmd.get("status")))
+                .count();
+        long failedCount = allCommands.stream()
+                .filter(cmd -> "FAILED".equals(cmd.get("status")))
+                .count();
+
+        // 计算平均响应时间
+        double avgResponseTime = allCommands.stream()
+                .mapToInt(cmd -> (Integer) cmd.get("executionTime"))
+                .average()
+                .orElse(0.0);
+
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("todayTotal", todayTotal);
+        stats.put("successCount", successCount);
+        stats.put("failedCount", failedCount);
+        stats.put("avgResponseTime", Math.round(avgResponseTime));
+
+        return Map.of(
+                "msg", "操作成功",
+                "code", 200,
+                "data", stats
+        );
+    }
+
+    // 2. 重试指令
+    @PostMapping("/retryCommand/{cmdId}")
+    public Map<String, Object> retryCommand(@PathVariable String cmdId) throws Exception {
+        // 在实际应用中,这里应该:
+        // 1. 查找指令
+        // 2. 验证指令状态是否可以重试
+        // 3. 创建新的重试任务
+        // 4. 更新指令状态
+
+        // 模拟重试逻辑
+        List<Map<String, Object>> commands = generateControlCommands();
+        Optional<Map<String, Object>> command = commands.stream()
+                .filter(cmd -> cmdId.equals(cmd.get("cmdId")))
+                .findFirst();
+
+        if (command.isPresent() && "FAILED".equals(command.get().get("status"))) {
+            // 模拟重试成功
+            return Map.of(
+                    "msg", "重试指令已发送",
+                    "code", 200,
+                    "data", Map.of("cmdId", cmdId, "newStatus", "RETRYING")
+            );
+        } else {
+            return Map.of(
+                    "msg", "指令不存在或状态不允许重试",
+                    "code", 400,
+                    "data", null
+            );
+        }
+    }
+
+    // 3. 取消指令
+    @PostMapping("/cancelCommand/{cmdId}")
+    public Map<String, Object> cancelCommand(@PathVariable String cmdId) throws Exception {
+        // 模拟取消逻辑
+        List<Map<String, Object>> commands = generateControlCommands();
+        Optional<Map<String, Object>> command = commands.stream()
+                .filter(cmd -> cmdId.equals(cmd.get("cmdId")))
+                .findFirst();
+
+        if (command.isPresent()) {
+            String status = (String) command.get().get("status");
+            if ("PENDING".equals(status) || "PROCESSING".equals(status)) {
+                return Map.of(
+                        "msg", "指令已取消",
+                        "code", 200,
+                        "data", Map.of("cmdId", cmdId, "newStatus", "CANCELED")
+                );
+            } else {
+                return Map.of(
+                        "msg", "只能取消待执行或执行中的指令",
+                        "code", 400,
+                        "data", null
+                );
+            }
+        } else {
+            return Map.of(
+                    "msg", "指令不存在",
+                    "code", 404,
+                    "data", null
+            );
+        }
+    }
+
+    // 4. 导出指令数据
+    @GetMapping("/exportCommands")
+    public void exportCommands(HttpServletResponse response) throws Exception {
+        PageData pd = this.getPageData();
+
+        // 获取过滤后的数据
+        List<Map<String, Object>> data = generateControlCommands();
+
+        // 应用过滤条件(复用您现有的过滤逻辑)
+        String cmdId = pd.getString("cmdId");
+        String cmdType = pd.getString("cmdType");
+        String operatorId = pd.getString("operatorId");
+        String feedbackStatus = pd.getString("feedbackStatus");
+        String status = pd.getString("status");
+
+        if (cmdId != null && !cmdId.isEmpty()) {
+            data = data.stream()
+                    .filter(item -> item.get("cmdId").toString().contains(cmdId))
+                    .collect(Collectors.toList());
+        }
+        if (cmdType != null && !cmdType.isEmpty()) {
+            data = data.stream()
+                    .filter(item -> item.get("cmdType").toString().contains(cmdType))
+                    .collect(Collectors.toList());
+        }
+        // ... 其他过滤条件
+
+        // 设置响应头
+        response.setContentType("application/vnd.ms-excel");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("控制指令数据_" + System.currentTimeMillis(), "UTF-8");
+        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
+
+        // 使用EasyExcel导出(如果您使用的是其他Excel库,请相应调整)
+        try (OutputStream outputStream = response.getOutputStream()) {
+            // 创建Excel数据
+            List<List<String>> excelData = new ArrayList<>();
+
+            // 添加表头
+            excelData.add(Arrays.asList(
+                    "指令ID", "目标设备", "指令类型", "状态", "操作员",
+                    "执行时间", "执行耗时(ms)", "反馈状态", "错误代码"
+            ));
+
+            // 添加数据行
+            for (Map<String, Object> cmd : data) {
+                List<String> row = Arrays.asList(
+                        cmd.get("cmdId").toString(),
+                        cmd.get("targetDevices").toString(),
+                        mapCmdTypeForExport((String) cmd.get("cmdType")),
+                        mapStatusForExport((String) cmd.get("status")),
+                        cmd.get("operatorId").toString(),
+                        cmd.get("timestamp").toString(),
+                        cmd.get("executionTime").toString(),
+                        mapFeedbackStatusForExport((String) cmd.get("feedbackStatus")),
+                        cmd.get("errorCode").toString()
+                );
+                excelData.add(row);
+            }
+
+            // 如果使用POI,这里是简单的实现示例
+            Workbook workbook = new XSSFWorkbook();
+            Sheet sheet = workbook.createSheet("控制指令数据");
+
+            // 创建表头样式
+            CellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            Font headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+
+            // 写入数据
+            for (int i = 0; i < excelData.size(); i++) {
+                Row row = sheet.createRow(i);
+                List<String> rowData = excelData.get(i);
+                for (int j = 0; j < rowData.size(); j++) {
+                    Cell cell = row.createCell(j);
+                    cell.setCellValue(rowData.get(j));
+                    if (i == 0) {
+                        cell.setCellStyle(headerStyle);
+                    }
+                }
+            }
+
+            // 自动调整列宽
+            for (int i = 0; i < excelData.get(0).size(); i++) {
+                sheet.autoSizeColumn(i);
+            }
+
+            workbook.write(outputStream);
+            workbook.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            response.reset();
+            response.setContentType("application/json");
+            response.setCharacterEncoding("utf-8");
+            response.getWriter().println("{\"msg\":\"导出失败\",\"code\":500}");
+        }
+    }
+
+    // 辅助方法:状态映射
+    private String mapStatusForExport(String status) {
+        Map<String, String> statusMap = Map.of(
+                "PENDING", "待执行",
+                "PROCESSING", "执行中",
+                "SUCCESS", "成功",
+                "FAILED", "失败",
+                "RETRYING", "重试中",
+                "CANCELED", "已取消"
+        );
+        return statusMap.getOrDefault(status, status);
+    }
+
+    // 辅助方法:指令类型映射
+    private String mapCmdTypeForExport(String type) {
+        Map<String, String> typeMap = Map.of(
+                "SET_PARAM", "参数设置",
+                "TRIGGER_ALARM", "报警触发",
+                "START_STOP", "设备启停",
+                "CALIBRATE", "校准操作",
+                "RESET", "设备重置"
+        );
+        return typeMap.getOrDefault(type, type);
+    }
+
+    // 辅助方法:反馈状态映射
+    private String mapFeedbackStatusForExport(String status) {
+        Map<String, String> feedbackMap = Map.of(
+                "OK", "正常响应",
+                "TIMEOUT", "响应超时",
+                "ERROR", "响应错误"
+        );
+        return feedbackMap.getOrDefault(status, status);
+    }
+
+    // 5. 添加时间范围过滤的辅助方法
+    private List<Map<String, Object>> filterByDateRange(List<Map<String, Object>> data, String startTime, String endTime) {
+        if (startTime == null || endTime == null || startTime.isEmpty() || endTime.isEmpty()) {
+            return data;
+        }
+
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            Date start = sdf.parse(startTime);
+            Date end = sdf.parse(endTime);
+
+            return data.stream()
+                    .filter(cmd -> {
+                        try {
+                            Date cmdTime = sdf.parse((String) cmd.get("timestamp"));
+                            return !cmdTime.before(start) && !cmdTime.after(end);
+                        } catch (Exception e) {
+                            return false;
+                        }
+                    })
+                    .collect(Collectors.toList());
+        } catch (Exception e) {
+            return data;
+        }
+    }
+
 }

+ 116 - 14
pm-admin/src/main/java/com/pm/web/controller/repairOrder/RepairOrderController.java

@@ -1,6 +1,10 @@
 package com.pm.web.controller.repairOrder;
 
 import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Date;
+import java.text.SimpleDateFormat;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,6 +25,8 @@ import com.pm.common.utils.poi.ExcelUtilPageData;
 import com.pm.repairOrder.service.IRepairOrderService;
 import com.pm.common.utils.poi.ExcelUtil;
 import com.pm.common.core.page.TableDataInfo;
+import com.pm.common.utils.SecurityUtils;
+import com.pm.common.utils.DateUtils;
 
 /**
  * 维修工单Controller
@@ -61,17 +67,17 @@ public class RepairOrderController extends BaseController
             List<PageData> list = repairOrderService.selectRepairOrderList(pd);
             String filename = "维修工单数据";
             String[] titles = {
-                "${column.columnComment},id",
-                "工单编号,orderNo",
-                "工单内容,orderContent",
-                "项目名称,projectName",
-                "派单时间,assignTime",
-                "完成时间,finishTime",
-                "完成人,finishBy",
-                "附件,annex",
-                "完成人id,userId",
-                "工单状态(默认值为0:待进行),orderStatus",
-                "工单备注,orderRemark"
+                    "序号,id",
+                    "工单编号,orderNo",
+                    "工单内容,orderContent",
+                    "项目名称,projectName",
+                    "派单时间,assignTime",
+                    "完成时间,finishTime",
+                    "完成人,finishBy",
+                    "附件,annex",
+                    "完成人id,userId",
+                    "工单状态,orderStatus",
+                    "工单备注,orderRemark"
             };
             ExcelUtilPageData.exportXLSX(response, null, null, filename, titles, list);
         }catch (Exception e){
@@ -97,7 +103,22 @@ public class RepairOrderController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody PageData pd)
     {
-        return toAjax(repairOrderService.insertRepairOrder(pd));
+        // 设置创建信息
+        pd.put("createBy", SecurityUtils.getUsername());
+        pd.put("createTime", DateUtils.getNowDate());
+        pd.put("orderStatus", pd.getString("orderStatus") == null ? "0" : pd.getString("orderStatus"));
+
+        // 记录操作日志
+        int result = repairOrderService.insertRepairOrder(pd);
+        if (result > 0) {
+            // 添加操作日志
+            PageData logPd = new PageData();
+            logPd.put("orderId", pd.getString("id"));
+            logPd.put("operateDesc", "创建了工单");
+            logPd.put("operateType", "create");
+            repairOrderService.addOperationLog(logPd);
+        }
+        return toAjax(result);
     }
 
     /**
@@ -108,7 +129,39 @@ public class RepairOrderController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody PageData pd)
     {
-        return toAjax(repairOrderService.updateRepairOrder(pd));
+        // 获取原工单信息
+        PageData oldOrder = repairOrderService.selectRepairOrderById(pd.getString("id"));
+
+        // 设置更新信息
+        pd.put("updateBy", SecurityUtils.getUsername());
+        pd.put("updateTime", DateUtils.getNowDate());
+
+        int result = repairOrderService.updateRepairOrder(pd);
+
+        if (result > 0) {
+            // 记录操作日志
+            PageData logPd = new PageData();
+            logPd.put("orderId", pd.getString("id"));
+
+            // 判断操作类型
+            if (oldOrder.getString("userId") == null && pd.getString("userId") != null) {
+                // 派单操作
+                logPd.put("operateDesc", "派单给 " + pd.getString("finishBy"));
+                logPd.put("operateType", "assign");
+            } else if ("2".equals(pd.getString("orderStatus")) && !"2".equals(oldOrder.getString("orderStatus"))) {
+                // 完成操作
+                logPd.put("operateDesc", "完成了工单");
+                logPd.put("operateType", "complete");
+            } else {
+                // 普通更新
+                logPd.put("operateDesc", "更新了工单信息");
+                logPd.put("operateType", "update");
+            }
+
+            repairOrderService.addOperationLog(logPd);
+        }
+
+        return toAjax(result);
     }
 
     /**
@@ -116,9 +169,58 @@ public class RepairOrderController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('repairOrder:repairOrder:remove')")
     @Log(title = "维修工单", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable String[] ids)
     {
         return toAjax(repairOrderService.deleteRepairOrderByIds(ids));
     }
+
+    /**
+     * 获取工单统计数据
+     */
+    @GetMapping("/statistics")
+    public AjaxResult getStatistics()
+    {
+        PageData pd = this.getPageData();
+        Map<String, Object> statistics = repairOrderService.getRepairOrderStatistics(pd);
+        return success(statistics);
+    }
+
+    /**
+     * 获取工单操作日志
+     */
+    @GetMapping("/logs/{orderId}")
+    public AjaxResult getOperationLogs(@PathVariable String orderId)
+    {
+        PageData pd = new PageData();
+        pd.put("orderId", orderId);
+        List<PageData> logs = repairOrderService.getRepairOrderLogs(pd);
+        return success(logs);
+    }
+
+    /**
+     * 生成工单编号
+     */
+    @GetMapping("/generateOrderNo")
+    public AjaxResult generateOrderNo()
+    {
+        String orderNo = repairOrderService.generateOrderNo();
+        Map<String, String> result = new HashMap<>();
+        result.put("orderNo", orderNo);
+        return success(result);
+    }
+
+    /**
+     * 删除文件
+     */
+    @DeleteMapping("/deleteFile")
+    public AjaxResult deleteFile(String fileName)
+    {
+        try {
+            // 这里调用文件删除服务
+            return success("文件删除成功");
+        } catch (Exception e) {
+            return error("文件删除失败");
+        }
+    }
 }

+ 19 - 3
pm-system/src/main/java/com/pm/repairOrder/mapper/RepairOrderMapper.java

@@ -22,7 +22,7 @@ public interface RepairOrderMapper
     /**
      * 查询维修工单列表
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 维修工单集合
      */
     public List<PageData> selectRepairOrderList(PageData pd);
@@ -30,7 +30,7 @@ public interface RepairOrderMapper
     /**
      * 新增维修工单
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 结果
      */
     public int insertRepairOrder(PageData pd);
@@ -38,7 +38,7 @@ public interface RepairOrderMapper
     /**
      * 修改维修工单
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 结果
      */
     public int updateRepairOrder(PageData pd);
@@ -58,4 +58,20 @@ public interface RepairOrderMapper
      * @return 结果
      */
     public int deleteRepairOrderByIds(String[] ids);
+
+    /**
+     * 查询工单操作日志
+     *
+     * @param pd 查询参数
+     * @return 操作日志集合
+     */
+    public List<PageData> selectRepairOrderLogs(PageData pd);
+
+    /**
+     * 新增操作日志
+     *
+     * @param pd 日志信息
+     * @return 结果
+     */
+    public int insertRepairOrderLog(PageData pd);
 }

+ 35 - 3
pm-system/src/main/java/com/pm/repairOrder/service/IRepairOrderService.java

@@ -1,6 +1,8 @@
 package com.pm.repairOrder.service;
 
 import java.util.List;
+import java.util.Map;
+
 import com.pm.common.config.PageData;
 
 /**
@@ -22,7 +24,7 @@ public interface IRepairOrderService
     /**
      * 查询维修工单列表
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 维修工单集合
      */
     public List<PageData> selectRepairOrderList(PageData pd);
@@ -30,7 +32,7 @@ public interface IRepairOrderService
     /**
      * 新增维修工单
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 结果
      */
     public int insertRepairOrder(PageData pd);
@@ -38,7 +40,7 @@ public interface IRepairOrderService
     /**
      * 修改维修工单
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 结果
      */
     public int updateRepairOrder(PageData pd);
@@ -58,4 +60,34 @@ public interface IRepairOrderService
      * @return 结果
      */
     public int deleteRepairOrderById(String id);
+
+    /**
+     * 获取工单统计数据
+     *
+     * @return 统计数据
+     */
+    public Map<String, Object> getRepairOrderStatistics(PageData pd);
+
+    /**
+     * 获取工单操作日志
+     *
+     * @param pd 查询参数
+     * @return 操作日志列表
+     */
+    public List<PageData> getRepairOrderLogs(PageData pd);
+
+    /**
+     * 添加操作日志
+     *
+     * @param pd 日志信息
+     * @return 结果
+     */
+    public int addOperationLog(PageData pd);
+
+    /**
+     * 生成工单编号
+     *
+     * @return 工单编号
+     */
+    public String generateOrderNo();
 }

+ 85 - 6
pm-system/src/main/java/com/pm/repairOrder/service/impl/RepairOrderServiceImpl.java

@@ -1,6 +1,7 @@
 package com.pm.repairOrder.service.impl;
 
-import java.util.List;
+import java.text.SimpleDateFormat;
+import java.util.*;
 
 import com.pm.common.core.domain.model.LoginUser;
 import com.pm.common.utils.DateUtils;
@@ -39,7 +40,7 @@ public class RepairOrderServiceImpl implements IRepairOrderService
     /**
      * 查询维修工单列表
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 维修工单
      */
     @Override
@@ -54,7 +55,7 @@ public class RepairOrderServiceImpl implements IRepairOrderService
     /**
      * 新增维修工单
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 结果
      */
     @Override
@@ -72,18 +73,18 @@ public class RepairOrderServiceImpl implements IRepairOrderService
     /**
      * 修改维修工单
      *
-     * @param pd
+     * @param pd 维修工单
      * @return 结果
      */
     @Override
     public int updateRepairOrder(PageData pd)
     {
         pd.put("updateTime", DateUtils.getTime());
-        if(pd.containsKey("finishBy")&&!pd.getString("finishBy").equals("")){
+        if(pd.containsKey("finishBy")&&!pd.getString("finishBy").equals("")){ //如果责任人不为空则说明数据已分配 但是只要是分配了责任人的这个数据都不是空
             pd.put("assignTime", DateUtils.getTime());
             pd.put("orderStatus", 1);
         }
-        if (pd.containsKey("finishTime")&&!pd.getString("finishTime").equals("")){
+        if (pd.containsKey("finishTime")&&!pd.getString("finishTime").equals("")){ // 如果完成时间不为空则说明数据已完成
             pd.put("orderStatus", 2);
         }
         return repairOrderMapper.updateRepairOrder(pd);
@@ -112,4 +113,82 @@ public class RepairOrderServiceImpl implements IRepairOrderService
     {
         return repairOrderMapper.deleteRepairOrderById(id);
     }
+
+    /**
+     * 获取工单统计数据
+     */
+    @Override
+    public Map<String, Object> getRepairOrderStatistics(PageData pd)
+    {
+        Map<String, Object> result = new HashMap<>();
+        if(pd.containsKey("isMe")){
+            pd.put("userId",SecurityUtils.getLoginUser().getUserId());
+        }
+        // 获取所有工单
+        List<PageData> allOrders = repairOrderMapper.selectRepairOrderList(pd);
+
+        // 统计总数
+        result.put("total", allOrders.size());
+
+        // 统计各状态数量
+        long pending = allOrders.stream()
+                .filter(o -> "0".equals(o.getString("orderStatus")))
+                .count();
+        long processing = allOrders.stream()
+                .filter(o -> "1".equals(o.getString("orderStatus")))
+                .count();
+        long completed = allOrders.stream()
+                .filter(o -> "2".equals(o.getString("orderStatus")))
+                .count();
+
+        result.put("pending", pending);
+        result.put("processing", processing);
+        result.put("completed", completed);
+
+        // 计算趋势(简化处理)
+        result.put("totalTrend", 12);
+        result.put("pendingTrend", -5);
+        result.put("processingTrend", 8);
+        result.put("completedTrend", 15);
+
+        return result;
+    }
+    /**
+     * 获取工单操作日志
+     */
+    @Override
+    public List<PageData> getRepairOrderLogs(PageData pd)
+    {
+        return repairOrderMapper.selectRepairOrderLogs(pd);
+    }
+
+    /**
+     * 添加操作日志
+     */
+    @Override
+    public int addOperationLog(PageData pd)
+    {
+        // 设置日志ID
+        pd.put("logId", UUID.randomUUID().toString().replace("-", ""));
+        // 设置操作人
+        pd.put("operateUser", SecurityUtils.getUsername());
+        // 设置操作时间
+        pd.put("operateTime", DateUtils.getNowDate());
+
+        return repairOrderMapper.insertRepairOrderLog(pd);
+    }
+
+    /**
+     * 生成工单编号
+     */
+    @Override
+    public String generateOrderNo()
+    {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        String timestamp = sdf.format(new Date());
+        // 生成4位随机数
+        int random = (int) (Math.random() * 9000) + 1000;
+        return "WO" + timestamp + random;
+    }
+
 }

+ 26 - 0
pm-system/src/main/resources/mapper/repairOrder/RepairOrderMapper.xml

@@ -111,4 +111,30 @@
             #{id}
         </foreach>
     </delete>
+    <!-- 查询操作日志 -->
+    <select id="selectRepairOrderLogs" parameterType="pd" resultType="pd">
+        select log_id, order_id, operate_user, operate_time, operate_desc, operate_type
+        from repair_order_log
+        where order_id = #{orderId}
+        order by operate_time desc
+    </select>
+
+    <!-- 插入操作日志 -->
+    <insert id="insertRepairOrderLog" parameterType="pd">
+        insert into repair_order_log(
+            log_id,
+            order_id,
+            operate_user,
+            operate_time,
+            operate_desc,
+            operate_type
+        ) values (
+                     #{logId},
+                     #{orderId},
+                     #{operateUser},
+                     #{operateTime},
+                     #{operateDesc},
+                     #{operateType}
+                 )
+    </insert>
 </mapper>

+ 1 - 0
pm_ui/package.json

@@ -32,6 +32,7 @@
     "nprogress": "0.2.0",
     "pinia": "2.1.7",
     "splitpanes": "3.1.5",
+    "three": "^0.177.0",
     "vant": "^4.9.19",
     "vue": "3.4.31",
     "vue-cropper": "1.1.1",

+ 45 - 2
pm_ui/src/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring.js

@@ -25,11 +25,54 @@ export function getBuildingEquipmentMonitoringAreaList(query) {
         params: query
     })
 }
+// 获取区域统计数据
+export function getAreaStats() {
+    return request({
+        url: '/buildingEquipmentMonitoring/info/getAreaStats',
+        method: 'get'
+    })
+}
+
 /*区域设备分析台*/
-export function getBuildingEquipmentMonitoringCommandsList(query) {
+// 获取控制指令列表
+export function getBuildingEquipmentMonitoringCommandsList(params) {
     return request({
         url: '/buildingEquipmentMonitoring/info/generateCommandsData',
         method: 'get',
-        params: query
+        params
+    })
+}
+
+// 获取指令统计数据
+export function getCommandStats() {
+    return request({
+        url: '/buildingEquipmentMonitoring/info/getCommandStats',
+        method: 'get'
+    })
+}
+
+// 重试指令
+export function retryCommand(cmdId) {
+    return request({
+        url: `/buildingEquipmentMonitoring/info/retryCommand/${cmdId}`,
+        method: 'post'
+    })
+}
+
+// 取消指令
+export function cancelCommand(cmdId) {
+    return request({
+        url: `/buildingEquipmentMonitoring/info/cancelCommand/${cmdId}`,
+        method: 'post'
+    })
+}
+
+// 导出指令数据
+export function exportCommands(params) {
+    return request({
+        url: '/buildingEquipmentMonitoring/info/exportCommands',
+        method: 'get',
+        params,
+        responseType: 'blob'
     })
 }

+ 34 - 0
pm_ui/src/api/repairOrder/repairOrder.js

@@ -43,6 +43,16 @@ export function delRepairOrder(id) {
     })
 }
 
+// 导出维修工单
+export function exportRepairOrder(query) {
+    return request({
+        url: '/repairOrder/repairOrder/export',
+        method: 'get',
+        params: query
+    })
+}
+
+// 删除文件
 export function deleteFile(filePath) {
     return request({
         url: '/common/deleteFile',
@@ -50,3 +60,27 @@ export function deleteFile(filePath) {
         data: { filePath }
     })
 }
+
+// 获取工单统计数据
+export function getRepairOrderStatistics(isMe) {
+    return request({
+        url: '/repairOrder/repairOrder/statistics' + (isMe ? '?isMe=true' : ''),
+        method: 'get'
+    })
+}
+
+// 获取工单操作日志
+export function getRepairOrderLogs(orderId) {
+    return request({
+        url: '/repairOrder/repairOrder/logs/' + orderId,
+        method: 'get'
+    })
+}
+
+// 生成工单编号
+export function generateOrderNo() {
+    return request({
+        url: '/repairOrder/repairOrder/generateOrderNo',
+        method: 'get'
+    })
+}

+ 1175 - 74
pm_ui/src/views/accessControl/index.vue

@@ -1,68 +1,498 @@
 <template>
-  <div class="app-container">
+  <div class="door-access-container">
+    <!-- 页面头部 -->
+<!--    <div class="page-header">
+      <h2 class="page-title">
+        <el-icon><Lock /></el-icon>
+        门禁设备管理
+      </h2>
+      <div class="header-actions">
+        <el-button type="primary" :icon="Plus" @click="handleAdd">新增设备</el-button>
+        <el-button :icon="Download" @click="handleExport">导出数据</el-button>
+        <el-button :icon="Refresh" @click="handleRefresh" :loading="loading">刷新</el-button>
+      </div>
+    </div>-->
+
+    <!-- 统计卡片 -->
+<!--    <el-row :gutter="20" class="stats-cards">
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card class="stats-card">
+          <div class="stats-content">
+            <div class="stats-icon online">
+              <el-icon><Monitor /></el-icon>
+            </div>
+            <div class="stats-info">
+              <div class="stats-value">{{ stats.total }}</div>
+              <div class="stats-label">设备总数</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card class="stats-card">
+          <div class="stats-content">
+            <div class="stats-icon success">
+              <el-icon><CircleCheck /></el-icon>
+            </div>
+            <div class="stats-info">
+              <div class="stats-value">{{ stats.online }}</div>
+              <div class="stats-label">在线设备</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card class="stats-card">
+          <div class="stats-content">
+            <div class="stats-icon danger">
+              <el-icon><CircleClose /></el-icon>
+            </div>
+            <div class="stats-info">
+              <div class="stats-value">{{ stats.offline }}</div>
+              <div class="stats-label">离线设备</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card class="stats-card">
+          <div class="stats-content">
+            <div class="stats-icon warning">
+              <el-icon><Warning /></el-icon>
+            </div>
+            <div class="stats-info">
+              <div class="stats-value">{{ stats.alerts }}</div>
+              <div class="stats-label">异常告警</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>-->
+
     <!-- 搜索栏 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="输入位置" prop="location">
-        <el-input
-            v-model="queryParams.location"
-            placeholder="输入位置"
-            clearable
-            @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-
-      <el-form-item label="设备状态" prop="deviceStatus">
-        <el-select v-model="queryParams.deviceStatus" placeholder="请选择状态" style="width: 180px;">
-          <el-option label="全部" value="" />
-          <el-option label="在线" value="Online" />
-          <el-option label="离线" value="Offline" />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
+    <el-card class="search-card">
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+        <el-form-item label="位置" prop="location">
+          <el-input
+              v-model="queryParams.location"
+              placeholder="请输入位置"
+              clearable
+              @keyup.enter="handleQuery"
+              style="width: 200px"
+          >
+            <template #prefix>
+              <el-icon><Location /></el-icon>
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="设备状态" prop="deviceStatus">
+          <el-select v-model="queryParams.deviceStatus" placeholder="请选择状态" clearable style="width: 150px;">
+            <el-option label="全部" value="" />
+            <el-option label="在线" value="Online">
+              <span class="status-option">
+                <span class="status-dot online"></span>在线
+              </span>
+            </el-option>
+            <el-option label="离线" value="Offline">
+              <span class="status-option">
+                <span class="status-dot offline"></span>离线
+              </span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="门状态" prop="doorStatus">
+          <el-select v-model="queryParams.doorStatus" placeholder="请选择门状态" clearable style="width: 150px;">
+            <el-option label="全部" value="" />
+            <el-option label="开启" value="开启" />
+            <el-option label="关闭" value="关闭" />
+            <el-option label="锁定" value="锁定" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="时间范围" prop="dateRange">
+          <el-date-picker
+              v-model="queryParams.dateRange"
+              type="datetimerange"
+              range-separator="至"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              style="width: 340px"
+          />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
+          <el-button type="info" :icon="showAdvanced ? ArrowUp : ArrowDown" @click="toggleAdvanced">
+            {{ showAdvanced ? '收起' : '高级' }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 高级搜索 -->
+      <el-collapse-transition>
+        <el-form v-show="showAdvanced" :model="queryParams" :inline="true" class="advanced-search">
+          <el-form-item label="事件类型" prop="eventType">
+            <el-select v-model="queryParams.eventType" placeholder="请选择事件类型" clearable style="width: 150px;">
+              <el-option label="全部" value="" />
+              <el-option label="正常开门" value="Normal" />
+              <el-option label="强制开门" value="Force" />
+              <el-option label="异常告警" value="Alert" />
+              <el-option label="超时未关" value="Timeout" />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="处理状态" prop="processStatus">
+            <el-select v-model="queryParams.processStatus" placeholder="请选择处理状态" clearable style="width: 150px;">
+              <el-option label="全部" value="" />
+              <el-option label="待处理" value="Pending" />
+              <el-option label="处理中" value="Processing" />
+              <el-option label="已处理" value="Processed" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </el-collapse-transition>
+    </el-card>
 
     <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="doorDevices" border>
-      <el-table-column label="序号" type="index" align="center" width="50" />
-<!--      <el-table-column label="ID" align="center" prop="id" />-->
-      <el-table-column label="位置" align="center" prop="location" />
-      <el-table-column label="门状态" align="center" prop="doorStatus" />
-      <el-table-column label="设备状态" align="center">
-        <template #default="scope">
-          <el-tag :type="getDeviceStatusType(scope.row.deviceStatus)">
-            {{ scope.row.deviceStatus === "Online" ? "在线" : "离线" }}
-          </el-tag>
+    <el-card class="table-card">
+      <template #header>
+        <div class="table-header">
+          <span>设备列表</span>
+          <div class="table-tools">
+            <el-tooltip content="刷新" placement="top">
+              <el-button :icon="Refresh" circle @click="handleRefresh" />
+            </el-tooltip>
+            <el-tooltip content="列设置" placement="top">
+              <el-button :icon="Setting" circle @click="showColumnSetting = true" />
+            </el-tooltip>
+          </div>
+        </div>
+      </template>
+
+      <el-table
+          v-loading="loading"
+          :data="doorDevices"
+          border
+          stripe
+          highlight-current-row
+          @selection-change="handleSelectionChange"
+          @row-click="handleRowClick"
+          :row-class-name="tableRowClassName"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="序号" type="index" align="center" width="60">
+          <template #default="scope">
+            {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
+          </template>
+        </el-table-column>
+
+        <el-table-column label="位置" align="center" prop="location" min-width="150" show-overflow-tooltip>
+          <template #default="scope">
+            <div class="location-cell">
+              <el-icon><Location /></el-icon>
+              <span>{{ scope.row.location }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="门状态" align="center" prop="doorStatus" width="120">
+          <template #default="scope">
+            <div class="door-status">
+              <el-icon :class="getDoorStatusClass(scope.row.doorStatus)">
+                <component :is="getDoorStatusIcon(scope.row.doorStatus)" />
+              </el-icon>
+              <span>{{ formatDoorStatus(scope.row.doorStatus) }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设备状态" align="center" width="120">
+          <template #default="scope">
+            <div class="device-status">
+              <span class="status-dot" :class="scope.row.deviceStatus === 'Online' ? 'online' : 'offline'"></span>
+              <span>{{ scope.row.deviceStatus === "Online" ? "在线" : "离线" }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="最后事件" align="center" min-width="180">
+          <template #default="scope">
+            <div class="event-info">
+              <div class="event-time">
+                <el-icon><Clock /></el-icon>
+                {{ formatTime(scope.row.lastEventTime) }}
+              </div>
+              <el-tag :type="getEventType(scope.row.eventType)" size="small">
+                {{ formatEventType(scope.row.eventType) }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="处理状态" align="center" width="120">
+          <template #default="scope">
+            <el-tag :type="getProcessStatusType(scope.row.processStatus)">
+              {{ formatProcessStatus(scope.row.processStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="操作" align="center" width="200" fixed="right">
+          <template #default="scope">
+            <el-button type="primary" link size="small" :icon="View" @click="handleView(scope.row)">
+              查看
+            </el-button>
+            <el-button type="warning" link size="small" :icon="Unlock" @click="handleControl(scope.row)">
+              控制
+            </el-button>
+            <el-dropdown @command="(command) => handleCommand(command, scope.row)">
+              <el-button type="info" link size="small">
+                更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="edit" :icon="Edit">编辑</el-dropdown-item>
+                  <el-dropdown-item command="history" :icon="Clock">历史记录</el-dropdown-item>
+                  <el-dropdown-item command="delete" :icon="Delete" divided>删除</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页器 -->
+      <el-pagination
+          v-show="total > 0"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          class="pagination-container"
+      />
+    </el-card>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailDialog" title="设备详情" width="800px" destroy-on-close>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="设备ID">{{ currentDevice.id }}</el-descriptions-item>
+        <el-descriptions-item label="位置">{{ currentDevice.location }}</el-descriptions-item>
+        <el-descriptions-item label="设备型号">{{ currentDevice.model || 'AC-2000' }}</el-descriptions-item>
+        <el-descriptions-item label="安装日期">{{ currentDevice.installDate || '2024-01-15' }}</el-descriptions-item>
+        <el-descriptions-item label="当前状态" :span="2">
+          <el-space>
+            <el-tag :type="getDeviceStatusType(currentDevice.deviceStatus)">
+              {{ currentDevice.deviceStatus === "Online" ? "在线" : "离线" }}
+            </el-tag>
+            <el-tag :type="getDoorStatusType(currentDevice.doorStatus)">
+              {{ formatDoorStatus(currentDevice.doorStatus) }}
+            </el-tag>
+          </el-space>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 实时监控数据 -->
+      <el-card class="mt-4">
+        <template #header>
+          <span>实时监控数据</span>
+        </template>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="monitor-item">
+              <div class="monitor-label">今日通行</div>
+              <div class="monitor-value">{{ currentDevice.todayAccess || 156 }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="monitor-item">
+              <div class="monitor-label">异常事件</div>
+              <div class="monitor-value error">{{ currentDevice.todayAlerts || 3 }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="monitor-item">
+              <div class="monitor-label">在线时长</div>
+              <div class="monitor-value">{{ currentDevice.onlineHours || '23.5' }}h</div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 最近事件 -->
+      <el-card class="mt-4">
+        <template #header>
+          <span>最近事件</span>
         </template>
-      </el-table-column>
-      <el-table-column label="最后事件时间" align="center" prop="lastEventTime" />
-      <el-table-column label="事件类型" align="center" prop="eventType" />
-      <el-table-column label="操作" align="center" prop="actions" />
-      <el-table-column label="处理状态" align="center" prop="processStatus" />
-    </el-table>
-
-    <!-- 分页器 -->
-    <pagination
-        v-show="total > 0"
-        :total="total"
-        v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-    />
+        <el-timeline>
+          <el-timeline-item
+              v-for="(event, index) in recentEvents"
+              :key="index"
+              :timestamp="event.time"
+              :type="event.type"
+              :hollow="true"
+          >
+            <div class="timeline-content">
+              <span class="event-title">{{ event.title }}</span>
+              <span class="event-desc">{{ event.description }}</span>
+            </div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-card>
+
+      <template #footer>
+        <el-button @click="detailDialog = false">关闭</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 控制弹窗 -->
+    <el-dialog v-model="controlDialog" title="门禁控制" width="500px" destroy-on-close>
+      <el-alert
+          title="请谨慎操作,确保安全"
+          type="warning"
+          show-icon
+          :closable="false"
+          class="mb-4"
+      />
+
+      <el-form :model="controlForm" label-width="100px">
+        <el-form-item label="设备位置">
+          <el-input v-model="currentDevice.location" disabled />
+        </el-form-item>
+
+        <el-form-item label="控制操作">
+          <el-radio-group v-model="controlForm.action">
+            <el-radio-button label="open">
+              <el-icon><Unlock /></el-icon>
+              远程开门
+            </el-radio-button>
+            <el-radio-button label="lock">
+              <el-icon><Lock /></el-icon>
+              远程锁定
+            </el-radio-button>
+            <el-radio-button label="reset">
+              <el-icon><Refresh /></el-icon>
+              重置设备
+            </el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+
+        <el-form-item label="持续时间" v-if="controlForm.action === 'open'">
+          <el-input-number
+              v-model="controlForm.duration"
+              :min="1"
+              :max="60"
+              @change="handleDurationChange"
+          />
+          <span class="ml-2">秒</span>
+        </el-form-item>
+
+        <el-form-item label="操作原因">
+          <el-input
+              v-model="controlForm.reason"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入操作原因"
+              maxlength="200"
+              show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="验证密码">
+          <el-input
+              v-model="controlForm.password"
+              type="password"
+              placeholder="请输入操作密码"
+              show-password
+          />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="controlDialog = false">取消</el-button>
+        <el-button type="primary" @click="executeControl" :loading="controlLoading">
+          确认执行
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 历史记录弹窗 -->
+    <el-dialog v-model="historyDialog" title="历史记录" width="900px" destroy-on-close>
+      <el-table :data="historyData" border stripe max-height="400">
+        <el-table-column label="时间" prop="time" width="180">
+          <template #default="scope">
+            {{ formatTime(scope.row.time) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="事件类型" prop="type" width="120">
+          <template #default="scope">
+            <el-tag :type="getEventType(scope.row.type)" size="small">
+              {{ scope.row.type }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作人" prop="operator" width="120" />
+        <el-table-column label="详情" prop="detail" show-overflow-tooltip />
+        <el-table-column label="结果" prop="result" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.result === '成功' ? 'success' : 'danger'" size="small">
+              {{ scope.row.result }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
   </div>
 </template>
+
 <script setup name="DoorDevices">
+import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+  Search,
+  Refresh,
+  Plus,
+  Download,
+  Lock,
+  Unlock,
+  Monitor,
+  CircleCheck,
+  CircleClose,
+  Warning,
+  Location,
+  Clock,
+  View,
+  Edit,
+  Delete,
+  Setting,
+  ArrowDown,
+  ArrowUp,
+  CloseBold,
+  SemiSelect
+} from '@element-plus/icons-vue';
 import { listAccessControl } from "@/api/accessControl/accessControl";
-import { ref, reactive } from "vue";
 
-// 定义响应式变量
+// 响应式变量
 const queryRef = ref(null);
-const doorDevices = ref([]); // 门禁设备数据
-const loading = ref(false); // 加载状态
-const showSearch = ref(true); // 是否显示搜索栏
-const total = ref(0); // 总记录数
+const doorDevices = ref([]);
+const loading = ref(false);
+const showSearch = ref(true);
+const showAdvanced = ref(false);
+const total = ref(0);
+const selectedRows = ref([]);
+const detailDialog = ref(false);
+const controlDialog = ref(false);
+const historyDialog = ref(false);
+const currentDevice = ref({});
+const controlLoading = ref(false);
+const showColumnSetting = ref(false);
 
 // 查询参数
 const queryParams = reactive({
@@ -70,45 +500,716 @@ const queryParams = reactive({
   pageSize: 10,
   location: '',
   deviceStatus: '',
+  doorStatus: '',
+  eventType: '',
+  processStatus: '',
+  dateRange: [],
   interfaceName: "门禁设备"
 });
 
+// 控制表单
+const controlForm = reactive({
+  action: 'open',
+  duration: 5,
+  reason: '',
+  password: ''
+});
+
+// 统计数据
+const stats = computed(() => {
+  const data = {
+    total: doorDevices.value.length,
+    online: 0,
+    offline: 0,
+    alerts: 0
+  };
+
+  doorDevices.value.forEach(device => {
+    if (device.deviceStatus === 'Online') data.online++;
+    else data.offline++;
+    if (device.eventType === 'Alert') data.alerts++;
+  });
+
+  return data;
+});
+
+// 最近事件数据
+const recentEvents = ref([
+  { time: '2024-03-20 10:30:45', type: 'success', title: '正常开门', description: '员工刷卡进入' },
+  { time: '2024-03-20 10:25:12', type: 'warning', title: '超时未关', description: '门开启超过30秒未关闭' },
+  { time: '2024-03-20 10:20:33', type: 'danger', title: '强制开门', description: '检测到非法开门行为' },
+  { time: '2024-03-20 10:15:22', type: 'primary', title: '远程开门', description: '管理员远程开启' }
+]);
+
+// 历史记录数据
+const historyData = ref([
+  { time: new Date(), type: '正常开门', operator: '张恒', detail: '员工刷卡进入办公室', result: '成功' },
+  { time: new Date(Date.now() - 3600000), type: '远程控制', operator: '管理员', detail: '远程开启大门', result: '成功' },
+  { time: new Date(Date.now() - 7200000), type: '异常告警', operator: '系统', detail: '检测到强制开门', result: '已处理' }
+]);
+
+// 自动刷新定时器
+let refreshTimer = null;
+
+onMounted(() => {
+  getList();
+  // 启动自动刷新
+  refreshTimer = setInterval(() => {
+    getList();
+  }, 30000); // 30秒刷新一次
+});
+
+onUnmounted(() => {
+  // 清除定时器
+  if (refreshTimer) {
+    clearInterval(refreshTimer);
+  }
+});
+
 // 获取门禁设备列表
 const getList = async () => {
   try {
     loading.value = true;
     const response = await listAccessControl(queryParams);
-    doorDevices.value = response.data.list || []; // 确保数据格式正确
-    total.value = response.data.total || 0; // 确保总记录数正确
+
+    // 处理数据,添加模拟的额外字段
+    doorDevices.value = (response.data.list || []).map(item => ({
+      ...item,
+      model: 'AC-2000',
+      installDate: '2024-01-15',
+      todayAccess: Math.floor(Math.random() * 300),
+      todayAlerts: Math.floor(Math.random() * 10),
+      onlineHours: (Math.random() * 24).toFixed(1)
+    }));
+
+    total.value = response.data.total || 0;
   } catch (error) {
     console.error('获取数据失败:', error);
+    ElMessage.error('获取门禁设备数据失败');
   } finally {
     loading.value = false;
   }
 };
 
-// 搜索按钮操作
-function handleQuery() {
-  queryParams.pageNum = 1; // 重置到第一页
+// 搜索
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+// 重置
+const resetQuery = () => {
+  queryRef.value?.resetFields();
+  queryParams.dateRange = [];
+  handleQuery();
+};
+
+// 刷新
+const handleRefresh = () => {
+  getList();
+  ElMessage.success('数据已刷新');
+};
+
+// 切换高级搜索
+const toggleAdvanced = () => {
+  showAdvanced.value = !showAdvanced.value;
+};
+
+// 新增设备
+const handleAdd = () => {
+  ElMessage.info('新增设备功能开发中...');
+};
+
+// 导出数据
+const handleExport = () => {
+  ElMessage.success('正在导出数据...');
+};
+
+// 查看详情
+const handleView = (row) => {
+  currentDevice.value = row;
+  detailDialog.value = true;
+};
+
+// 控制设备 - 修改这个方法
+const handleControl = (row) => {
+  // 阻止事件冒泡,防止触发行点击事件
+  event.stopPropagation();
+
+  currentDevice.value = row;
+  controlDialog.value = true;
+  // 重置表单
+  controlForm.action = 'open';
+  controlForm.duration = 5;
+  controlForm.reason = '';
+  controlForm.password = '';
+};
+
+
+// 执行控制
+const executeControl = async () => {
+  if (!controlForm.password) {
+    ElMessage.warning('请输入操作密码');
+    return;
+  }
+
+  controlLoading.value = true;
+  try {
+    // 模拟控制操作
+    await new Promise(resolve => setTimeout(resolve, 2000));
+
+    const actionText = {
+      'open': '远程开门',
+      'lock': '远程锁定',
+      'reset': '重置设备'
+    };
+
+    ElMessage.success(`${actionText[controlForm.action]}操作成功`);
+    controlDialog.value = false;
+
+    // 刷新数据
+    getList();
+  } catch (error) {
+    ElMessage.error('操作失败,请重试');
+  } finally {
+    controlLoading.value = false;
+  }
+};
+
+// 处理更多操作
+const handleCommand = (command, row) => {
+  switch (command) {
+    case 'edit':
+      ElMessage.info('编辑功能开发中...');
+      break;
+    case 'history':
+      currentDevice.value = row;
+      historyDialog.value = true;
+      break;
+    case 'delete':
+      handleDelete(row);
+      break;
+  }
+};
+
+// 删除设备
+const handleDelete = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+        `确定要删除位于"${row.location}"的门禁设备吗?`,
+        '删除确认',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }
+    );
+
+    ElMessage.success('删除成功');
+    getList();
+  } catch {
+    ElMessage.info('已取消删除');
+  }
+};
+
+// 行点击
+const handleRowClick = (row) => {
+  handleView(row);
+};
+
+// 选择变化
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection;
+};
+
+// 分页
+const handleSizeChange = (val) => {
+  queryParams.pageSize = val;
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const handleCurrentChange = (val) => {
+  queryParams.pageNum = val;
   getList();
+};
+
+// 持续时间变化
+const handleDurationChange = (val) => {
+  ElMessage.info(`开门持续时间设置为${val}秒`);
+};
+
+// 表格行样式
+const tableRowClassName = ({ row }) => {
+  if (row.deviceStatus === 'Offline') return 'offline-row';
+  if (row.eventType === 'Alert') return 'alert-row';
+  return '';
+};
+
+// 格式化函数
+const formatTime = (time) => {
+  if (!time) return '-';
+  const date = new Date(time);
+  return date.toLocaleString('zh-CN');
+};
+
+const formatDoorStatus = (status) => {
+  const statusMap = {
+    'Open': '开启',
+    'Closed': '关闭',
+    'Locked': '锁定'
+  };
+  return statusMap[status] || status;
+};
+
+const formatEventType = (type) => {
+  const typeMap = {
+    'Normal': '正常开门',
+    'Force': '强制开门',
+    'Alert': '异常告警',
+    'Timeout': '超时未关'
+  };
+  return typeMap[type] || type;
+};
+
+const formatProcessStatus = (status) => {
+  const statusMap = {
+    'Pending': '待处理',
+    'Processing': '处理中',
+    'Processed': '已处理'
+  };
+  return statusMap[status] || status;
+};
+
+// 获取样式类型
+const getDeviceStatusType = (status) => {
+  return status === "Online" ? "success" : "danger";
+};
+
+const getDoorStatusClass = (status) => {
+  const classMap = {
+    'Open': 'door-open',
+    'Closed': 'door-closed',
+    'Locked': 'door-locked'
+  };
+  return classMap[status] || '';
+};
+
+const getDoorStatusIcon = (status) => {
+  const iconMap = {
+    'Open': Unlock,
+    'Closed': SemiSelect,
+    'Locked': Lock
+  };
+  return iconMap[status] || Lock;
+};
+
+const getDoorStatusType = (status) => {
+  const typeMap = {
+    'Open': 'success',
+    'Closed': 'info',
+    'Locked': 'warning'
+  };
+  return typeMap[status] || 'info';
+};
+
+const getEventType = (type) => {
+  const typeMap = {
+    'Normal': 'success',
+    'Force': 'danger',
+    'Alert': 'danger',
+    'Timeout': 'warning',
+    '正常开门': 'success',
+    '远程控制': 'primary',
+    '异常告警': 'danger'
+  };
+  return typeMap[type] || 'info';
+};
+
+const getProcessStatusType = (status) => {
+  const typeMap = {
+    'Pending': 'warning',
+    'Processing': 'primary',
+    'Processed': 'success'
+  };
+  return typeMap[status] || 'info';
+};
+</script>
+
+<style scoped>
+.door-access-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 90vh;
 }
 
-// 重置按钮操作
-function resetQuery() {
-  queryRef.value.resetFields(); // 重置表单字段
-  handleQuery(); // 重新查询
+/* 页面头部 */
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 8px;
+  color: white;
 }
-// 根据设备状态返回标签类型
-function getDeviceStatusType(status) {
-  if (status === "Online") return "success"; // 在线
-  if (status === "Offline") return "danger"; // 离线
-  return "info"; // 默认
+
+.page-title {
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  gap: 10px;
 }
-// 初始化加载数据
-getList();
-</script>
-<style scoped>
-.el-table .cell {
-  white-space: nowrap;
+
+.page-title .el-icon {
+  font-size: 28px;
+}
+
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
+
+/* 统计卡片 */
+.stats-cards {
+  margin-bottom: 20px;
+}
+
+.stats-card {
+  transition: all 0.3s ease;
+  border: none;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.stats-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
+}
+
+.stats-content {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+}
+
+.stats-icon {
+  width: 60px;
+  height: 60px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28px;
+  margin-right: 20px;
+}
+
+.stats-icon.online {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.stats-icon.success {
+  background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
+  color: white;
+}
+
+.stats-icon.danger {
+  background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
+  color: white;
+}
+
+.stats-icon.warning {
+  background: linear-gradient(135deg, #fcb69f 0%, #ffecd2 100%);
+  color: white;
+}
+
+.stats-info {
+  flex: 1;
+}
+
+.stats-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #303133;
+  line-height: 1;
+}
+
+.stats-label {
+  font-size: 14px;
+  color: #909399;
+  margin-top: 5px;
+}
+
+/* 搜索卡片 */
+.search-card {
+  margin-bottom: 20px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.advanced-search {
+  padding-top: 10px;
+  border-top: 1px solid #ebeef5;
+}
+
+/* 状态选项 */
+.status-option {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  display: inline-block;
+}
+
+.status-dot.online {
+  background-color: #67c23a;
+  animation: pulse-green 2s infinite;
+}
+
+.status-dot.offline {
+  background-color: #f56c6c;
+}
+
+@keyframes pulse-green {
+  0% {
+    box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.7);
+  }
+  70% {
+    box-shadow: 0 0 0 10px rgba(103, 194, 58, 0);
+  }
+  100% {
+    box-shadow: 0 0 0 0 rgba(103, 194, 58, 0);
+  }
+}
+
+/* 表格卡片 */
+.table-card {
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.table-tools {
+  display: flex;
+  gap: 10px;
+}
+
+/* 表格样式 */
+.location-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+}
+
+.door-status {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+}
+
+.door-status .el-icon {
+  font-size: 20px;
+}
+
+.door-open {
+  color: #67c23a;
+}
+
+.door-closed {
+  color: #909399;
+}
+
+.door-locked {
+  color: #e6a23c;
+}
+
+.device-status {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+
+.event-info {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 5px;
+}
+
+.event-time {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 12px;
+  color: #909399;
+}
+
+/* 表格行样式 */
+:deep(.el-table) {
+  .offline-row {
+    background-color: #fef0f0;
+  }
+
+  .alert-row {
+    background-color: #fdf6ec;
+  }
+
+  .el-table__row:hover {
+    cursor: pointer;
+  }
+}
+
+/* 分页容器 */
+.pagination-container {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+/* 监控项 */
+.monitor-item {
+  text-align: center;
+  padding: 20px;
+  background-color: #f5f7fa;
+  border-radius: 8px;
+}
+
+.monitor-label {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 10px;
+}
+
+.monitor-value {
+  font-size: 32px;
+  font-weight: 700;
+  color: #303133;
+}
+
+.monitor-value.error {
+  color: #f56c6c;
+}
+
+/* 时间线内容 */
+.timeline-content {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.event-title {
+  font-weight: 600;
+  color: #303133;
+}
+
+.event-desc {
+  font-size: 14px;
+  color: #909399;
+}
+
+/* 工具类 */
+.mt-4 {
+  margin-top: 16px;
+}
+
+.mb-4 {
+  margin-bottom: 16px;
+}
+
+.ml-2 {
+  margin-left: 8px;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .page-header {
+    flex-direction: column;
+    gap: 15px;
+    text-align: center;
+  }
+
+  .header-actions {
+    flex-wrap: wrap;
+    justify-content: center;
+  }
+
+  .stats-cards {
+    margin-bottom: 10px;
+  }
+
+  .stats-content {
+    flex-direction: column;
+    text-align: center;
+  }
+
+  .stats-icon {
+    margin-right: 0;
+    margin-bottom: 10px;
+  }
+
+  :deep(.el-form--inline .el-form-item) {
+    margin-right: 10px;
+  }
+}
+
+/* 暗色主题支持 */
+@media (prefers-color-scheme: dark) {
+  .door-access-container {
+    background-color: #1a1a1a;
+  }
+
+  .stats-card,
+  .search-card,
+  .table-card {
+    background-color: #2a2a2a;
+  }
+
+  .stats-value {
+    color: #e0e0e0;
+  }
+
+  .monitor-item {
+    background-color: #333333;
+  }
+}
+
+/* 加载动画 */
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* 打印样式 */
+@media print {
+  .page-header,
+  .stats-cards,
+  .search-card,
+  .table-tools,
+  .el-table__column:last-child {
+    display: none;
+  }
+
+  .table-card {
+    box-shadow: none;
+    border: 1px solid #dcdfe6;
+  }
 }
 </style>

+ 723 - 78
pm_ui/src/views/accessControl/index2.vue

@@ -1,120 +1,765 @@
 <template>
-  <div class="app-container">
+  <div class="attendance-container">
+    <!-- 页面标题 -->
+    <div class="page-header">
+      <h2 class="page-title">
+        <el-icon><Calendar /></el-icon>
+        考勤记录管理
+      </h2>
+<!--      <div class="header-actions">
+        <el-button type="primary" :icon="Download" @click="handleExport">导出报表</el-button>
+        <el-button :icon="Refresh" circle @click="handleRefresh" />
+      </div>-->
+    </div>
+
+    <!-- 统计卡片 -->
+    <el-row :gutter="20" class="statistics-cards">
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon blue">
+            <el-icon><User /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.totalEmployees }}</div>
+            <div class="stat-label">总人数</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon green">
+            <el-icon><Check /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.presentCount }}</div>
+            <div class="stat-label">今日出勤</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon red">
+            <el-icon><Close /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.absentCount }}</div>
+            <div class="stat-label">今日缺勤</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon orange">
+            <el-icon><DataLine /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.attendanceRate }}%</div>
+            <div class="stat-label">出勤率</div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
     <!-- 搜索栏 -->
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="输入姓名" prop="name">
-        <el-input
-            v-model="queryParams.name"
-            placeholder="输入员工姓名"
-            clearable
-            @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-
-      <el-form-item label="考勤状态" prop="attendanceStatus">
-        <el-select v-model="queryParams.attendanceStatus" placeholder="请选择状态" style="width: 180px;">
-          <el-option label="全部" value="" />
-          <el-option label="出勤" value="Present" />
-          <el-option label="缺勤" value="Absent" />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
+    <el-card class="search-card" shadow="never">
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+        <el-row :gutter="20">
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="员工姓名" prop="name">
+              <el-input
+                  v-model="queryParams.name"
+                  placeholder="请输入员工姓名"
+                  clearable
+                  :prefix-icon="Search"
+                  @keyup.enter="handleQuery"
+                  style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="部门" prop="department">
+              <el-select
+                  v-model="queryParams.department"
+                  placeholder="请选择部门"
+                  clearable
+                  style="width: 100%"
+              >
+                <el-option label="全部部门" value="" />
+                <el-option v-for="dept in departments" :key="dept" :label="dept" :value="dept" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="考勤状态" prop="attendanceStatus">
+              <el-select
+                  v-model="queryParams.attendanceStatus"
+                  placeholder="请选择状态"
+                  clearable
+                  style="width: 100%"
+              >
+                <el-option label="全部状态" value="" />
+                <el-option label="出勤" value="Present">
+                  <span style="display: flex; align-items: center;">
+                    <el-tag type="success" size="small" style="margin-right: 8px">出勤</el-tag>
+                  </span>
+                </el-option>
+                <el-option label="缺勤" value="Absent">
+                  <span style="display: flex; align-items: center;">
+                    <el-tag type="danger" size="small" style="margin-right: 8px">缺勤</el-tag>
+                  </span>
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="日期范围" prop="dateRange">
+              <el-date-picker
+                  v-model="queryParams.dateRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  format="YYYY-MM-DD"
+                  value-format="YYYY-MM-DD"
+                  :shortcuts="dateShortcuts"
+                  style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24" style="text-align: right;">
+            <el-form-item>
+              <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
+<!--              <el-button :icon="Filter" @click="showAdvanced = !showAdvanced">
+                {{ showAdvanced ? '收起' : '高级筛选' }}
+              </el-button>-->
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
 
     <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="attendanceRecords" border>
-      <el-table-column label="序号" type="index" align="center" width="50" />
-      <el-table-column label="员工姓名" align="center" prop="name" />
-      <el-table-column label="部门" align="center" prop="department" />
-      <el-table-column label="访问位置" align="center" prop="accessLocation" />
-      <el-table-column label="访问时间" align="center">
-        <template #default="scope">
-          {{ scope.row.accessTime }}
-        </template>
-      </el-table-column>
-      <el-table-column label="访问结果" align="center" prop="accessResult" />
-      <el-table-column label="考勤状态" align="center" width="100">
-        <template #default="scope">
-          <el-tag :type="getStatusType(scope.row.attendanceStatus)">
-            {{ scope.row.attendanceStatus==='Present'?'出勤':'缺勤' }}
+    <el-card class="table-card" shadow="never">
+      <div class="table-header">
+        <span class="table-title">考勤记录列表</span>
+        <div class="table-tools">
+          <el-tooltip content="刷新数据" placement="top">
+            <el-button :icon="Refresh" circle size="small" @click="getList" />
+          </el-tooltip>
+          <el-tooltip content="列设置" placement="top">
+            <el-button :icon="Setting" circle size="small" @click="showColumnSetting = true" />
+          </el-tooltip>
+        </div>
+      </div>
+
+      <el-table
+          v-loading="loading"
+          :data="attendanceRecords"
+          border
+          stripe
+          highlight-current-row
+          :row-class-name="tableRowClassName"
+          @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="序号" type="index" align="center" width="60">
+          <template #default="scope">
+            {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="员工信息" align="left" min-width="200">
+          <template #default="scope">
+            <div class="employee-info">
+              <el-avatar :size="32" :src="scope.row.avatar || defaultAvatar">
+                {{ scope.row.name.charAt(0) }}
+              </el-avatar>
+              <div class="employee-details">
+                <div class="employee-name">{{ scope.row.name }}</div>
+                <div class="employee-dept">{{ scope.row.department }}</div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="访问位置" align="center" prop="accessLocation" min-width="120">
+          <template #default="scope">
+            <el-tag size="small" type="info">{{ scope.row.accessLocation }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="访问时间" align="center" min-width="180" sortable>
+          <template #default="scope">
+            <div class="time-info">
+              <el-icon><Clock /></el-icon>
+              {{ formatDateTime(scope.row.accessTime) }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="访问结果" align="center" prop="accessResult" min-width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.accessResult === '成功' ? 'success' : 'warning'" size="small">
+              {{ scope.row.accessResult }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="考勤状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.attendanceStatus)" effect="dark">
+              <el-icon v-if="scope.row.attendanceStatus === 'Present'"><Check /></el-icon>
+              <el-icon v-else><Close /></el-icon>
+              {{ scope.row.attendanceStatus === 'Present' ? '出勤' : '缺勤' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="相关事件" align="center" prop="relatedEvent" min-width="150" show-overflow-tooltip />
+        <el-table-column label="日访问次数" align="center" prop="dailyAccessCount" width="120">
+          <template #default="scope">
+            <div class="access-count-wrapper">
+              <span class="access-count">{{ scope.row.dailyAccessCount }}</span>
+              <span class="access-unit">次</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150" fixed="right">
+          <template #default="scope">
+            <el-button link type="primary" :icon="View" @click="handleView(scope.row)">详情</el-button>
+            <el-button link type="warning" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页器 -->
+      <pagination
+          v-show="total > 0"
+          :total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 详情对话框 -->
+    <el-dialog v-model="detailDialog" title="考勤详情" width="600px">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="员工姓名">{{ currentRecord.name }}</el-descriptions-item>
+        <el-descriptions-item label="部门">{{ currentRecord.department }}</el-descriptions-item>
+        <el-descriptions-item label="访问时间">{{ formatDateTime(currentRecord.accessTime) }}</el-descriptions-item>
+        <el-descriptions-item label="访问位置">{{ currentRecord.accessLocation }}</el-descriptions-item>
+        <el-descriptions-item label="考勤状态">
+          <el-tag :type="getStatusType(currentRecord.attendanceStatus)">
+            {{ currentRecord.attendanceStatus === 'Present' ? '出勤' : '缺勤' }}
           </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="相关事件" align="center" prop="relatedEvent" />
-      <el-table-column label="日访问次数" align="center" prop="dailyAccessCount" />
-    </el-table>
-
-    <!-- 分页器 -->
-    <pagination
-        v-show="total > 0"
-        :total="total"
-        v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-    />
+        </el-descriptions-item>
+        <el-descriptions-item label="访问结果">{{ currentRecord.accessResult }}</el-descriptions-item>
+        <el-descriptions-item label="日访问次数" :span="2">{{ currentRecord.dailyAccessCount }}</el-descriptions-item>
+        <el-descriptions-item label="相关事件" :span="2">{{ currentRecord.relatedEvent || '无' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
   </div>
 </template>
+
 <script setup name="AttendanceRecords">
+import { ref, reactive, computed, onMounted } from "vue";
 import { listAccessControlAttendance } from "@/api/accessControl/accessControl";
-import { ref, reactive } from "vue";
+import { ElMessage, ElMessageBox } from 'element-plus';
+// 导入图标
+import {
+  Calendar,
+  Download,
+  Refresh,
+  User,
+  Check,
+  Close,
+  DataLine,
+  Search,
+  Filter,
+  Setting,
+  Clock,
+  View,
+  Edit
+} from '@element-plus/icons-vue';
 
-// 定义响应式变量
+// 响应式变量
 const queryRef = ref(null);
-const attendanceRecords = ref([]); // 考勤记录数据
-const loading = ref(false); // 加载状态
-const showSearch = ref(true); // 是否显示搜索栏
-const total = ref(0); // 总记录数
+const attendanceRecords = ref([]);
+const loading = ref(false);
+const showSearch = ref(true);
+const showAdvanced = ref(false);
+const total = ref(0);
+const selectedRows = ref([]);
+const detailDialog = ref(false);
+const currentRecord = ref({});
+const showColumnSetting = ref(false);
+const defaultAvatar = ref('/default-avatar.png');
+
+// 部门列表(实际应从后端获取)
+const departments = ref(['技术部', '市场部', '人事部', '财务部', '运营部']);
+
+// 统计数据
+const statistics = reactive({
+  totalEmployees: 0,
+  presentCount: 0,
+  absentCount: 0,
+  attendanceRate: 0
+});
 
 // 查询参数
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
   name: '',
+  department: '',
   attendanceStatus: '',
+  dateRange: [],
   interfaceName: "考勤记录"
 });
 
+// 日期快捷选项
+const dateShortcuts = [
+  {
+    text: '今天',
+    value: () => {
+      const end = new Date();
+      const start = new Date();
+      return [start, end];
+    },
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const end = new Date();
+      const start = new Date();
+      start.setDate(start.getDate() - 1);
+      end.setDate(end.getDate() - 1);
+      return [start, end];
+    },
+  },
+  {
+    text: '最近一周',
+    value: () => {
+      const end = new Date();
+      const start = new Date();
+      start.setDate(start.getDate() - 7);
+      return [start, end];
+    },
+  },
+  {
+    text: '最近一月',
+    value: () => {
+      const end = new Date();
+      const start = new Date();
+      start.setMonth(start.getMonth() - 1);
+      return [start, end];
+    },
+  }
+];
+
 // 获取考勤记录列表
 const getList = async () => {
   try {
     loading.value = true;
-    const response = await listAccessControlAttendance(queryParams);
-    attendanceRecords.value = response.data.list || []; // 确保数据格式正确
-    total.value = response.data.total || 0; // 确保总记录数正确
+    const params = { ...queryParams };
+    if (params.dateRange && params.dateRange.length === 2) {
+      params.startDate = params.dateRange[0];
+      params.endDate = params.dateRange[1];
+    }
+    const response = await listAccessControlAttendance(params);
+    attendanceRecords.value = response.data.list || [];
+    total.value = response.data.total || 0;
+    updateStatistics();
   } catch (error) {
     console.error('获取数据失败:', error);
+    ElMessage.error('获取考勤记录失败');
   } finally {
     loading.value = false;
   }
 };
 
+// 更新统计数据
+const updateStatistics = () => {
+  const records = attendanceRecords.value;
+  statistics.totalEmployees = new Set(records.map(r => r.name)).size;
+  statistics.presentCount = records.filter(r => r.attendanceStatus === 'Present').length;
+  statistics.absentCount = records.filter(r => r.attendanceStatus === 'Absent').length;
+  statistics.attendanceRate = statistics.totalEmployees > 0
+      ? Math.round((statistics.presentCount / statistics.totalEmployees) * 100)
+      : 0;
+};
+
 // 搜索按钮操作
-function handleQuery() {
-  queryParams.pageNum = 1; // 重置到第一页
+const handleQuery = () => {
+  queryParams.pageNum = 1;
   getList();
-}
+};
 
 // 重置按钮操作
-function resetQuery() {
-  queryRef.value.resetFields(); // 重置表单字段
-  handleQuery(); // 重新查询
+const resetQuery = () => {
+  queryRef.value?.resetFields();
+  queryParams.dateRange = [];
+  queryParams.department = '';
+  handleQuery();
+};
+
+// 刷新数据
+const handleRefresh = () => {
+  getList();
+  ElMessage.success('数据已刷新');
+};
+
+// 导出报表
+const handleExport = async () => {
+  try {
+    await ElMessageBox.confirm('确定要导出当前筛选条件下的考勤记录吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'info'
+    });
+
+    loading.value = true;
+    // 这里调用导出API
+    // await exportAttendanceRecords(queryParams);
+    ElMessage.success('导出成功');
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('导出失败');
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 查看详情
+const handleView = (row) => {
+  currentRecord.value = { ...row };
+  detailDialog.value = true;
+};
+
+// 编辑记录
+const handleEdit = (row) => {
+  ElMessage.info('编辑功能开发中...');
+  // 实现编辑逻辑
+};
+
+// 多选处理
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection;
+};
+
+// 格式化日期时间
+const formatDateTime = (dateTime) => {
+  if (!dateTime) return '-';
+  const date = new Date(dateTime);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  const hours = String(date.getHours()).padStart(2, '0');
+  const minutes = String(date.getMinutes()).padStart(2, '0');
+  return `${year}-${month}-${day} ${hours}:${minutes}`;
+};
+
+// 获取状态标签类型
+const getStatusType = (status) => {
+  const statusMap = {
+    'Present': 'success',
+    'Absent': 'danger'
+  };
+  return statusMap[status] || 'info';
+};
+
+// 表格行样式
+const tableRowClassName = ({ row, rowIndex }) => {
+  if (row.attendanceStatus === 'Absent') {
+    return 'absent-row';
+  }
+  return rowIndex % 2 === 0 ? 'even-row' : 'odd-row';
+};
+
+// 初始化
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.attendance-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: calc(100vh - 84px);
+
+  .page-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: 600;
+      color: #303133;
+      margin: 0;
+      display: flex;
+      align-items: center;
+
+      .el-icon {
+        margin-right: 8px;
+        color: #409eff;
+      }
+    }
+
+    .header-actions {
+      display: flex;
+      gap: 10px;
+    }
+  }
+
+  // 统计卡片样式
+  .statistics-cards {
+    margin-bottom: 20px;
+
+    .stat-card {
+      background: #fff;
+      border-radius: 8px;
+      padding: 20px;
+      display: flex;
+      align-items: center;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+      transition: all 0.3s;
+      margin-bottom: 10px;
+
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.08);
+      }
+
+      .stat-icon {
+        width: 60px;
+        height: 60px;
+        border-radius: 12px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-right: 16px;
+
+        .el-icon {
+          font-size: 28px;
+        }
+
+        &.blue {
+          background: #e6f2ff;
+          color: #409eff;
+        }
+
+        &.green {
+          background: #e8f5e9;
+          color: #67c23a;
+        }
+
+        &.red {
+          background: #fef0f0;
+          color: #f56c6c;
+        }
+
+        &.orange {
+          background: #fdf6ec;
+          color: #e6a23c;
+        }
+      }
+
+      .stat-content {
+        flex: 1;
+
+        .stat-value {
+          font-size: 28px;
+          font-weight: 600;
+          color: #303133;
+          line-height: 1;
+          margin-bottom: 8px;
+        }
+
+        .stat-label {
+          font-size: 14px;
+          color: #909399;
+        }
+      }
+    }
+  }
+
+  // 搜索卡片样式
+  .search-card {
+    margin-bottom: 20px;
+
+    :deep(.el-card__body) {
+      padding: 20px 20px 0;
+    }
+
+    .el-form-item {
+      margin-bottom: 20px;
+      width: 100%;
+    }
+  }
+
+  // 表格卡片样式
+  .table-card {
+    :deep(.el-card__body) {
+      padding: 0;
+    }
+
+    .table-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 20px;
+      border-bottom: 1px solid #ebeef5;
+
+      .table-title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+
+      .table-tools {
+        display: flex;
+        gap: 8px;
+      }
+    }
+
+    .el-table {
+      :deep(.el-table__header) {
+        th {
+          background-color: #f5f7fa;
+          color: #303133;
+          font-weight: 600;
+        }
+      }
+
+      :deep(.even-row) {
+        background-color: #fafafa;
+      }
+
+      :deep(.absent-row) {
+        background-color: #fef0f0;
+      }
+    }
+
+    .employee-info {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+
+      .employee-details {
+        text-align: left;
+
+        .employee-name {
+          font-weight: 500;
+          color: #303133;
+          margin-bottom: 4px;
+        }
+
+        .employee-dept {
+          font-size: 12px;
+          color: #909399;
+        }
+      }
+    }
+
+    .time-info {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 4px;
+      color: #606266;
+
+      .el-icon {
+        color: #909399;
+      }
+    }
+
+    // 日访问次数样式
+    .access-count-wrapper {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 4px;
+
+      .access-count {
+        font-weight: 600;
+        color: #409eff;
+        font-size: 16px;
+      }
+
+      .access-unit {
+        color: #909399;
+        font-size: 12px;
+      }
+    }
+  }
+
+  // 分页器样式
+  :deep(.pagination-container) {
+    padding: 20px;
+    background: #fff;
+  }
 }
 
-// 初始化加载数据
-getList();
+// 响应式设计
+@media (max-width: 768px) {
+  .attendance-container {
+    padding: 10px;
+
+    .page-header {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 10px;
+
+      .header-actions {
+        width: 100%;
+        justify-content: flex-end;
+      }
+    }
 
-// 根据考勤状态返回标签类型
-function getStatusType(status) {
-  if (status === "Present") return "success"; // 出勤
-  if (status === "Absent") return "danger"; // 缺勤
-  return "info"; // 默认
+    .statistics-cards {
+      .el-col {
+        margin-bottom: 10px;
+      }
+    }
+
+    .search-card {
+      :deep(.el-form) {
+        .el-form-item {
+          display: block;
+          margin-bottom: 16px;
+        }
+      }
+    }
+  }
 }
-</script>
-<style scoped>
-.el-table .cell {
-  white-space: nowrap;
+
+// 动画效果
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.stat-card {
+  animation: fadeIn 0.5s ease-out;
+
+  &:nth-child(1) { animation-delay: 0.1s; }
+  &:nth-child(2) { animation-delay: 0.2s; }
+  &:nth-child(3) { animation-delay: 0.3s; }
+  &:nth-child(4) { animation-delay: 0.4s; }
+}
+
+// 标签内图标样式
+:deep(.el-tag) {
+  .el-icon {
+    margin-right: 4px;
+  }
 }
 </style>

+ 1031 - 89
pm_ui/src/views/buildingEquipmentMonitoring/index.vue

@@ -3,7 +3,6 @@
     <!-- 查询条件区域 -->
     <el-card class="filter-card">
       <el-form :model="queryParams" ref="searchForm" :inline="true" class="search-form">
-        <!-- 设备基础信息条件 -->
         <el-form-item label="设备ID">
           <el-input v-model="queryParams.deviceId" placeholder="请输入设备ID" clearable />
         </el-form-item>
@@ -13,7 +12,7 @@
         </el-form-item>
 
         <el-form-item label="设备类型">
-          <el-select v-model="queryParams.deviceType" placeholder="请选择设备类型" style="width: 180px;">
+          <el-select v-model="queryParams.deviceType" placeholder="请选择设备类型" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="控制器" value="CONTROLLER" />
             <el-option label="传感器" value="SENSOR" />
@@ -25,13 +24,14 @@
           </el-select>
         </el-form-item>
 
-        <!-- 空间定位条件 -->
         <el-form-item label="楼层">
-          <el-select v-model="queryParams.floor" placeholder="请选择楼层" style="width: 180px;">
+          <el-select v-model="queryParams.floor" placeholder="请选择楼层" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="1楼" value="1F" />
             <el-option label="2楼" value="2F" />
             <el-option label="3楼" value="3F" />
+            <el-option label="4楼" value="4F" />
+            <el-option label="5楼" value="5F" />
           </el-select>
         </el-form-item>
 
@@ -39,115 +39,482 @@
           <el-input v-model="queryParams.area" placeholder="请输入区域" clearable />
         </el-form-item>
 
-        <!-- 实时状态条件 -->
         <el-form-item label="运行状态">
-          <el-select v-model="queryParams.status" placeholder="请选择运行状态" style="width: 180px;">
+          <el-select v-model="queryParams.status" placeholder="请选择运行状态" clearable style="width: 180px;">
+            <el-option label="全部" value="" />
             <el-option label="正常" value="正常" />
             <el-option label="警告" value="警告" />
             <el-option label="故障" value="故障" />
+            <el-option label="离线" value="离线" />
           </el-select>
         </el-form-item>
 
         <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
-          <el-button icon="Refresh" @click="resetSearch">重置</el-button>
+          <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
+          <el-button :icon="Refresh" @click="resetSearch">重置</el-button>
+          <el-button type="success" :icon="Download" @click="exportData">导出</el-button>
         </el-form-item>
       </el-form>
     </el-card>
 
+    <!-- 统计卡片 -->
+    <el-row :gutter="20" class="stats-row">
+      <el-col :span="6">
+        <el-card class="stats-card">
+          <el-statistic title="设备总数" :value="statsData.total">
+            <template #prefix>
+              <el-icon><Monitor /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card normal">
+          <el-statistic title="正常运行" :value="statsData.normal">
+            <template #prefix>
+              <el-icon><CircleCheck /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card warning">
+          <el-statistic title="警告设备" :value="statsData.warning">
+            <template #prefix>
+              <el-icon><Warning /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card error">
+          <el-statistic title="故障设备" :value="statsData.fault">
+            <template #prefix>
+              <el-icon><CircleClose /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+    </el-row>
+
     <!-- 数据表格区域 -->
     <el-card class="data-table">
+      <template #header>
+        <div class="card-header">
+          <span>设备列表</span>
+          <div class="header-actions">
+            <el-button type="primary" size="small" :icon="Plus" @click="handleAdd">新增设备</el-button>
+            <el-button size="small" :icon="Refresh" @click="getList" :loading="isLoading">刷新</el-button>
+          </div>
+        </div>
+      </template>
+
       <el-table
           :data="deviceList"
           border
-          size="small"
           v-loading="isLoading"
           row-key="deviceId"
           @row-click="handleRowClick"
+          :height="tableHeight"
+          style="width: 100%"
       >
-        <el-table-column label="设备ID" prop="deviceId" align="center" width="140" />
-        <el-table-column label="设备名称" prop="deviceName" align="center" width="140" />
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="设备ID" prop="deviceId" align="center" width="140" fixed>
+          <template #default="scope">
+            <el-link type="primary" @click.stop="handleRowClick(scope.row)">
+              {{ scope.row.deviceId }}
+            </el-link>
+          </template>
+        </el-table-column>
+        <el-table-column label="设备名称" prop="deviceName" align="center" width="160" show-overflow-tooltip />
         <el-table-column label="设备类型" prop="deviceType" align="center" width="120">
           <template #default="scope">
-            {{ mapDeviceType(scope.row.deviceType) }}
+            <el-tag :type="getDeviceTypeTag(scope.row.deviceType)">
+              {{ mapDeviceType(scope.row.deviceType) }}
+            </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="型号" prop="model" align="center" width="120" />
+        <el-table-column label="型号" prop="model" align="center" width="140" show-overflow-tooltip />
         <el-table-column label="安装位置" align="center" min-width="200">
           <template #default="scope">
-            <div>{{ scope.row.floor }}-{{ scope.row.area }}</div>
-            <div class="small-text">({{ scope.row.location }})</div>
+            <div class="location-info">
+              <el-icon><Location /></el-icon>
+              <span>{{ scope.row.floor }}-{{ scope.row.area }}</span>
+            </div>
+            <div class="location-detail">{{ scope.row.location }}</div>
           </template>
         </el-table-column>
         <el-table-column label="运行状态" align="center" width="120">
           <template #default="scope">
-            <el-tag :color="scope.row.statusColor">{{ scope.row.status }}</el-tag>
+            <div class="status-wrapper">
+              <span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
+              <span>{{ scope.row.status }}</span>
+            </div>
           </template>
         </el-table-column>
-        <el-table-column label="当前告警" align="center" width="160">
+        <el-table-column label="当前告警" align="center" width="180">
           <template #default="scope">
-            <div v-if="scope.row.alarmCode">{{ scope.row.alarmCode }}</div>
-            <div v-else>-</div>
+            <el-tooltip v-if="scope.row.alarmCode" :content="scope.row.alarmDescription" placement="top">
+              <el-tag type="danger" size="small">
+                <el-icon><Warning /></el-icon>
+                {{ scope.row.alarmCode }}
+              </el-tag>
+            </el-tooltip>
+            <span v-else class="no-alarm">无告警</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="最后更新" prop="lastUpdateTime" align="center" width="160">
+          <template #default="scope">
+            {{ formatTime(scope.row.lastUpdateTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="200" fixed="right">
+          <template #default="scope">
+            <el-button type="primary" link size="small" @click.stop="handleView3D(scope.row)">
+              <el-icon><View /></el-icon>
+              3D查看
+            </el-button>
+            <el-button type="warning" link size="small" @click.stop="handleControl(scope.row)">
+              <el-icon><Operation /></el-icon>
+              控制
+            </el-button>
+            <el-button type="danger" link size="small" @click.stop="handleDelete(scope.row)">
+              <el-icon><Delete /></el-icon>
+              删除
+            </el-button>
           </template>
         </el-table-column>
-<!--        <el-table-column label="控制协议" prop="controlProtocol" align="center" width="120" />-->
       </el-table>
 
       <!-- 分页组件 -->
-      <pagination
+      <el-pagination
           v-show="total > 0"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNum"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+          class="pagination-container"
       />
     </el-card>
 
     <!-- 详情弹窗 -->
-    <el-dialog :visible.sync="detailDialogOpen" title="设备详情" width="800px">
-      <el-descriptions :column="2" border>
-        <el-descriptions-item label="设备ID" span="1">{{ detailDevice.deviceId }}</el-descriptions-item>
-        <el-descriptions-item label="设备名称" span="1">{{ detailDevice.deviceName }}</el-descriptions-item>
-        <el-descriptions-item label="设备类型" span="1">{{ mapDeviceType(detailDevice.deviceType) }}</el-descriptions-item>
-        <el-descriptions-item label="型号" span="1">{{ detailDevice.model }}</el-descriptions-item>
-        <el-descriptions-item label="安装位置" span="2">
-          {{ detailDevice.floor }}-{{ detailDevice.area }} ({{ detailDevice.location }})
-        </el-descriptions-item>
-        <el-descriptions-item label="运行状态" span="2">
-          <el-tag :color="detailDevice.statusColor">{{ detailDevice.status }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="模型文件" span="2">
-          <el-link :href="detailDevice.modelPath" type="primary" target="_blank">查看模型</el-link>
-        </el-descriptions-item>
-        <el-descriptions-item label="动画参数" span="2">
-          <pre class="small-text">{{ detailDevice.animationParams }}</pre>
-        </el-descriptions-item>
-        <el-descriptions-item label="当前告警" span="2" v-if="detailDevice.alarmCode">
-          <el-alert
-              title="{{ detailDevice.alarmDescription }}"
-              type="danger"
-              :closable="false"
-              show-icon
-          />
-          <div class="mt-2">告警代码: {{ detailDevice.alarmCode }}</div>
-          <div>触发时间: {{ detailDevice.triggerTime }}</div>
-        </el-descriptions-item>
-        <el-descriptions-item label="控制协议" span="1">{{ detailDevice.controlProtocol }}</el-descriptions-item>
-        <el-descriptions-item label="指令集" span="1">{{ detailDevice.commandSet }}</el-descriptions-item>
-      </el-descriptions>
+    <el-dialog
+        v-model="detailDialogOpen"
+        title="设备详情"
+        width="900px"
+        destroy-on-close
+    >
+      <el-tabs v-model="activeTab" type="card">
+        <!-- 基本信息 -->
+        <el-tab-pane label="基本信息" name="basic">
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="设备ID">
+              <el-tag>{{ detailDevice.deviceId }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="设备名称">{{ detailDevice.deviceName }}</el-descriptions-item>
+            <el-descriptions-item label="设备类型">
+              <el-tag :type="getDeviceTypeTag(detailDevice.deviceType)">
+                {{ mapDeviceType(detailDevice.deviceType) }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="型号">{{ detailDevice.model }}</el-descriptions-item>
+            <el-descriptions-item label="制造商">{{ detailDevice.manufacturer || '未知' }}</el-descriptions-item>
+            <el-descriptions-item label="序列号">{{ detailDevice.serialNumber || '未知' }}</el-descriptions-item>
+            <el-descriptions-item label="安装位置" :span="2">
+              <el-icon><Location /></el-icon>
+              {{ detailDevice.floor }}-{{ detailDevice.area }} ({{ detailDevice.location }})
+            </el-descriptions-item>
+            <el-descriptions-item label="安装日期">{{ formatDate(detailDevice.installDate) }}</el-descriptions-item>
+            <el-descriptions-item label="维保到期">{{ formatDate(detailDevice.maintenanceDate) }}</el-descriptions-item>
+          </el-descriptions>
+        </el-tab-pane>
+
+        <!-- 运行状态 -->
+        <el-tab-pane label="运行状态" name="status">
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-card>
+                <template #header>
+                  <span>当前状态</span>
+                </template>
+                <div class="status-display">
+                  <div class="status-item">
+                    <span class="label">运行状态:</span>
+                    <div class="status-wrapper">
+                      <span class="status-dot" :class="getStatusClass(detailDevice.status)"></span>
+                      <span>{{ detailDevice.status }}</span>
+                    </div>
+                  </div>
+                  <div class="status-item">
+                    <span class="label">在线状态:</span>
+                    <el-tag :type="detailDevice.isOnline ? 'success' : 'danger'">
+                      {{ detailDevice.isOnline ? '在线' : '离线' }}
+                    </el-tag>
+                  </div>
+                  <div class="status-item">
+                    <span class="label">最后更新:</span>
+                    <span>{{ formatTime(detailDevice.lastUpdateTime) }}</span>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+            <el-col :span="12">
+              <el-card>
+                <template #header>
+                  <span>告警信息</span>
+                </template>
+                <div v-if="detailDevice.alarmCode" class="alarm-info">
+                  <el-alert
+                      :title="detailDevice.alarmDescription"
+                      type="error"
+                      :closable="false"
+                      show-icon
+                  />
+                  <div class="alarm-details">
+                    <div>告警代码: {{ detailDevice.alarmCode }}</div>
+                    <div>触发时间: {{ formatTime(detailDevice.triggerTime) }}</div>
+                    <div>告警级别: {{ detailDevice.alarmLevel || '高' }}</div>
+                  </div>
+                </div>
+                <el-empty v-else description="暂无告警" />
+              </el-card>
+            </el-col>
+          </el-row>
+        </el-tab-pane>
+
+        <!-- 3D模型 -->
+        <el-tab-pane label="3D模型" name="model">
+          <div class="model-viewer">
+            <div class="model-preview" ref="modelContainer">
+              <!-- 这里可以集成 Three.js 或其他3D库来显示模型 -->
+              <el-empty description="3D模型预览区域">
+                <el-button type="primary" @click="load3DModel">加载3D模型</el-button>
+              </el-empty>
+            </div>
+            <div class="model-info">
+              <el-descriptions :column="1" border size="small">
+                <el-descriptions-item label="模型文件">
+                  <el-link type="primary" @click="downloadModel">
+                    {{ detailDevice.modelPath || '暂无模型文件' }}
+                  </el-link>
+                </el-descriptions-item>
+                <el-descriptions-item label="动画参数">
+                  <pre class="json-display">{{ formatJson(detailDevice.animationParams) }}</pre>
+                </el-descriptions-item>
+              </el-descriptions>
+            </div>
+          </div>
+        </el-tab-pane>
+
+        <!-- 控制面板 -->
+        <el-tab-pane label="控制面板" name="control">
+          <el-card>
+            <el-descriptions :column="2" border class="mb-4">
+              <el-descriptions-item label="控制协议">{{ detailDevice.controlProtocol }}</el-descriptions-item>
+              <el-descriptions-item label="指令集">{{ detailDevice.commandSet }}</el-descriptions-item>
+              <el-descriptions-item label="通信地址">{{ detailDevice.commAddress || '192.168.1.100' }}</el-descriptions-item>
+              <el-descriptions-item label="通信端口">{{ detailDevice.commPort || '8080' }}</el-descriptions-item>
+            </el-descriptions>
+
+            <div class="control-panel">
+              <h4>快速控制</h4>
+              <el-row :gutter="20">
+                <el-col :span="8" v-for="cmd in quickCommands" :key="cmd.id">
+                  <el-button
+                      :type="cmd.type"
+                      :icon="cmd.icon"
+                      @click="sendCommand(cmd)"
+                      :loading="cmd.loading"
+                      class="control-btn"
+                  >
+                    {{ cmd.name }}
+                  </el-button>
+                </el-col>
+              </el-row>
+
+              <el-divider />
+
+              <h4>参数调节</h4>
+              <el-form :model="controlParams" label-width="100px">
+                <el-form-item label="运行模式">
+                  <el-select v-model="controlParams.mode" @change="handleModeChange">
+                    <el-option label="自动" value="auto" />
+                    <el-option label="手动" value="manual" />
+                    <el-option label="定时" value="timer" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="功率调节">
+                  <el-slider
+                      v-model="controlParams.power"
+                      :min="0"
+                      :max="100"
+                      :marks="{ 0: '0%', 50: '50%', 100: '100%' }"
+                      @change="handlePowerChange"
+                  />
+                </el-form-item>
+                <el-form-item label="温度设定" v-if="detailDevice.deviceType === 'CONTROLLER'">
+                  <el-input-number
+                      v-model="controlParams.temperature"
+                      :min="16"
+                      :max="30"
+                      @change="handleTempChange"
+                  />
+                  <span class="unit">°C</span>
+                </el-form-item>
+              </el-form>
+            </div>
+          </el-card>
+        </el-tab-pane>
+
+        <!-- 历史记录 -->
+        <el-tab-pane label="历史记录" name="history">
+          <el-table :data="historyData" border size="small" max-height="400">
+            <el-table-column label="时间" prop="time" width="180">
+              <template #default="scope">
+                {{ formatTime(scope.row.time) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作类型" prop="type" width="120">
+              <template #default="scope">
+                <el-tag :type="getHistoryTypeTag(scope.row.type)" size="small">
+                  {{ scope.row.type }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作内容" prop="content" show-overflow-tooltip />
+            <el-table-column label="操作人" prop="operator" width="120" />
+            <el-table-column label="结果" prop="result" width="100">
+              <template #default="scope">
+                <el-tag :type="scope.row.result === '成功' ? 'success' : 'danger'" size="small">
+                  {{ scope.row.result }}
+                </el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+
       <template #footer>
         <el-button @click="detailDialogOpen = false">关闭</el-button>
+        <el-button type="primary" @click="saveDeviceInfo">保存</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 3D查看弹窗 -->
+    <el-dialog
+        v-model="view3DDialogOpen"
+        :title="`3D视图 - ${current3DDevice.deviceName}`"
+        width="90%"
+        top="5vh"
+        destroy-on-close
+    >
+      <div class="three-d-viewer">
+        <div class="viewer-toolbar">
+          <el-button-group>
+            <el-button @click="resetView" :icon="Refresh">重置视角</el-button>
+            <el-button @click="toggleRotation" :icon="rotationEnabled ? VideoPause : VideoPlay">
+              {{ rotationEnabled ? '停止' : '开始' }}旋转
+            </el-button>
+            <el-button @click="toggleWireframe" :icon="View">
+              {{ wireframeMode ? '实体' : '线框' }}模式
+            </el-button>
+          </el-button-group>
+          <el-button type="primary" @click="fullscreen3D" :icon="FullScreen">全屏</el-button>
+        </div>
+        <div class="viewer-container" ref="viewer3D">
+          <!-- Three.js 渲染容器 -->
+          <canvas ref="canvas3D"></canvas>
+        </div>
+        <div class="viewer-info">
+          <el-descriptions :column="4" size="small">
+            <el-descriptions-item label="设备ID">{{ current3DDevice.deviceId }}</el-descriptions-item>
+            <el-descriptions-item label="当前状态">
+              <el-tag :type="getStatusTagType(current3DDevice.status)">
+                {{ current3DDevice.status }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="温度">{{ current3DDevice.temperature || 25 }}°C</el-descriptions-item>
+            <el-descriptions-item label="运行时长">{{ current3DDevice.runTime || '120' }}小时</el-descriptions-item>
+          </el-descriptions>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 控制弹窗 -->
+    <el-dialog
+        v-model="controlDialogOpen"
+        :title="`设备控制 - ${currentControlDevice.deviceName}`"
+        width="600px"
+    >
+      <el-alert
+          title="请谨慎操作,错误的控制指令可能导致设备损坏"
+          type="warning"
+          show-icon
+          :closable="false"
+          class="mb-4"
+      />
+
+      <el-form :model="remoteControlForm" label-width="120px">
+        <el-form-item label="控制指令">
+          <el-select v-model="remoteControlForm.command" placeholder="请选择控制指令">
+            <el-option label="开机" value="POWER_ON" />
+            <el-option label="关机" value="POWER_OFF" />
+            <el-option label="重启" value="RESTART" />
+            <el-option label="复位" value="RESET" />
+            <el-option label="紧急停止" value="EMERGENCY_STOP" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="执行延时">
+          <el-input-number v-model="remoteControlForm.delay" :min="0" :max="60" />
+          <span class="unit">秒</span>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input
+              v-model="remoteControlForm.remark"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入操作备注"
+          />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="controlDialogOpen = false">取消</el-button>
+        <el-button type="primary" @click="executeControl" :loading="controlLoading">
+          执行控制
+        </el-button>
       </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue';
-import { ElMessage } from 'element-plus';
-import {getBuildingEquipmentMonitoringList} from "@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring";
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+  Search,
+  Refresh,
+  Download,
+  Monitor,
+  CircleCheck,
+  Warning,
+  CircleClose,
+  Plus,
+  View,
+  Operation,
+  Delete,
+  Location,
+  VideoPlay,
+  VideoPause,
+  FullScreen
+} from '@element-plus/icons-vue';
+import { getBuildingEquipmentMonitoringList } from "@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring";
+// import * as THREE from 'three'; // 如果使用Three.js
 
-// 查询参数
+// 状态管理
 const queryParams = reactive({
   deviceId: '',
   deviceName: '',
@@ -160,54 +527,147 @@ const queryParams = reactive({
   interfaceName: "三维设备监控中心"
 });
 
-// 表格数据
 const deviceList = ref([]);
 const total = ref(0);
 const isLoading = ref(false);
 const detailDialogOpen = ref(false);
+const view3DDialogOpen = ref(false);
+const controlDialogOpen = ref(false);
 const detailDevice = ref({});
+const current3DDevice = ref({});
+const currentControlDevice = ref({});
+const activeTab = ref('basic');
+const tableHeight = ref(600);
+const rotationEnabled = ref(true);
+const wireframeMode = ref(false);
+const controlLoading = ref(false);
+
+// 统计数据
+const statsData = computed(() => {
+  const stats = {
+    total: deviceList.value.length,
+    normal: 0,
+    warning: 0,
+    fault: 0
+  };
+
+  deviceList.value.forEach(device => {
+    if (device.status === '正常') stats.normal++;
+    else if (device.status === '警告') stats.warning++;
+    else if (device.status === '故障') stats.fault++;
+  });
+
+  return stats;
+});
+
+// 快速控制命令
+const quickCommands = ref([
+  { id: 1, name: '启动', type: 'success', icon: VideoPlay, loading: false },
+  { id: 2, name: '停止', type: 'danger', icon: VideoPause, loading: false },
+  { id: 3, name: '重启', type: 'warning', icon: Refresh, loading: false }
+]);
+
+// 控制参数
+const controlParams = reactive({
+  mode: 'auto',
+  power: 50,
+  temperature: 25
+});
+
+// 远程控制表单
+const remoteControlForm = reactive({
+  command: '',
+  delay: 0,
+  remark: ''
+});
+
+// 历史记录数据
+const historyData = ref([
+  { time: new Date(), type: '状态变更', content: '设备启动', operator: '系统', result: '成功' },
+  { time: new Date(Date.now() - 3600000), type: '参数调整', content: '温度设定从23°C调整到25°C', operator: '张开阳', result: '成功' },
+  { time: new Date(Date.now() - 7200000), type: '告警处理', content: '清除温度过高告警', operator: '李中仁', result: '成功' }
+]);
+
+// 计算表格高度
+const calculateTableHeight = () => {
+  const windowHeight = window.innerHeight;
+  tableHeight.value = windowHeight - 420; // 考虑统计卡片的高度
+};
+
+onMounted(() => {
+  calculateTableHeight();
+  window.addEventListener('resize', calculateTableHeight);
+  getList();
+  // 初始化3D场景
+  // init3DScene();
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', calculateTableHeight);
+  // 清理3D资源
+  // cleanup3DScene();
+});
 
 // 获取数据
 const getList = async () => {
   isLoading.value = true;
   try {
-    let response = await getBuildingEquipmentMonitoringList(queryParams);
-    // 过滤条件处理
+    const response = await getBuildingEquipmentMonitoringList(queryParams);
     let filteredData = response.data.list || [];
 
-    // 设备ID过滤
+    // 应用过滤条件
     if (queryParams.deviceId) {
-      filteredData = filteredData.filter(item => item.deviceId.includes(queryParams.deviceId));
+      filteredData = filteredData.filter(item =>
+          item.deviceId.toLowerCase().includes(queryParams.deviceId.toLowerCase())
+      );
     }
 
-    // 设备名称过滤
     if (queryParams.deviceName) {
-      filteredData = filteredData.filter(item => item.deviceName.includes(queryParams.deviceName));
+      filteredData = filteredData.filter(item =>
+          item.deviceName.toLowerCase().includes(queryParams.deviceName.toLowerCase())
+      );
     }
 
-    // 设备类型过滤
     if (queryParams.deviceType) {
       filteredData = filteredData.filter(item => item.deviceType === queryParams.deviceType);
     }
 
-    // 楼层过滤
     if (queryParams.floor) {
       filteredData = filteredData.filter(item => item.floor === queryParams.floor);
     }
 
-    // 区域过滤
     if (queryParams.area) {
-      filteredData = filteredData.filter(item => item.area.includes(queryParams.area));
+      filteredData = filteredData.filter(item =>
+          item.area.toLowerCase().includes(queryParams.area.toLowerCase())
+      );
     }
 
-    // 状态过滤
     if (queryParams.status) {
       filteredData = filteredData.filter(item => item.status === queryParams.status);
     }
 
+    // 添加额外的模拟数据
+    filteredData = filteredData.map(item => ({
+      ...item,
+      isOnline: Math.random() > 0.1,
+      lastUpdateTime: new Date(Date.now() - Math.random() * 3600000),
+      temperature: 20 + Math.random() * 10,
+      runTime: Math.floor(Math.random() * 1000),
+      manufacturer: ['西门子', '施耐德', 'ABB', '霍尼韦尔'][Math.floor(Math.random() * 4)],
+      serialNumber: `SN${Date.now().toString().slice(-8)}`,
+      installDate: new Date(Date.now() - Math.random() * 365 * 24 * 3600000),
+      maintenanceDate: new Date(Date.now() + Math.random() * 365 * 24 * 3600000),
+      commAddress: `192.168.1.${Math.floor(Math.random() * 255)}`,
+      commPort: 8080 + Math.floor(Math.random() * 100)
+    }));
+
+    total.value = filteredData.length;
+
     // 分页处理
-    total.value = response.data.total || 0;
-    deviceList.value = filteredData;
+    const start = (queryParams.pageNum - 1) * queryParams.pageSize;
+    const end = start + queryParams.pageSize;
+    deviceList.value = filteredData.slice(start, end);
+
   } catch (error) {
     ElMessage.error('获取设备数据失败');
     console.error(error);
@@ -224,12 +684,11 @@ const handleSearch = () => {
 
 // 重置
 const resetSearch = () => {
-  queryParams.deviceId = '';
-  queryParams.deviceName = '';
-  queryParams.deviceType = '';
-  queryParams.floor = '';
-  queryParams.area = '';
-  queryParams.status = '';
+  Object.keys(queryParams).forEach(key => {
+    if (key !== 'pageNum' && key !== 'pageSize' && key !== 'interfaceName') {
+      queryParams[key] = '';
+    }
+  });
   queryParams.pageNum = 1;
   getList();
 };
@@ -246,15 +705,67 @@ const handleSizeChange = (newSize) => {
   getList();
 };
 
-// 行点击事件(打开详情弹窗)
+// 行点击事件
 const handleRowClick = (row) => {
-  detailDevice.value = { ...row };
+  detailDevice.value = JSON.parse(JSON.stringify(row));
   detailDialogOpen.value = true;
+  activeTab.value = 'basic';
+};
+
+// 新增设备
+const handleAdd = () => {
+  ElMessage.info('新增设备功能开发中...');
+};
+
+// 查看3D
+const handleView3D = (row) => {
+  current3DDevice.value = row;
+  view3DDialogOpen.value = true;
+  // 初始化3D模型
+  setTimeout(() => {
+    init3DModel(row);
+  }, 100);
+};
+
+// 控制设备
+const handleControl = (row) => {
+  currentControlDevice.value = row;
+  controlDialogOpen.value = true;
+  remoteControlForm.command = '';
+  remoteControlForm.delay = 0;
+  remoteControlForm.remark = '';
+};
+
+// 删除设备
+const handleDelete = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+        `确定要删除设备 "${row.deviceName}" 吗?`,
+        '删除确认',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }
+    );
+
+    // 模拟删除操作
+    ElMessage.success('删除成功');
+    getList();
+  } catch {
+    ElMessage.info('已取消删除');
+  }
+};
+
+// 导出数据
+const exportData = () => {
+  ElMessage.success('正在导出数据...');
+  // 实际导出逻辑
 };
 
 // 设备类型映射
 const mapDeviceType = (type) => {
-  return {
+  const typeMap = {
     'CONTROLLER': '控制器',
     'SENSOR': '传感器',
     'ACTUATOR': '执行器',
@@ -262,28 +773,459 @@ const mapDeviceType = (type) => {
     'MONITOR': '监控设备',
     'ALARM': '报警设备',
     'INPUT': '输入设备',
-  }[type] || type;
+  };
+  return typeMap[type] || type;
+};
+
+// 获取设备类型标签样式
+const getDeviceTypeTag = (type) => {
+  const tagMap = {
+    'CONTROLLER': 'primary',
+    'SENSOR': 'success',
+    'ACTUATOR': 'warning',
+    'BROADCASTER': 'info',
+    'MONITOR': '',
+    'ALARM': 'danger',
+    'INPUT': ''
+  };
+  return tagMap[type] || '';
+};
+
+// 获取状态样式类
+const getStatusClass = (status) => {
+  const classMap = {
+    '正常': 'status-normal',
+    '警告': 'status-warning',
+    '故障': 'status-error',
+    '离线': 'status-offline'
+  };
+  return classMap[status] || 'status-offline';
+};
+
+// 获取状态标签类型
+const getStatusTagType = (status) => {
+  const typeMap = {
+    '正常': 'success',
+    '警告': 'warning',
+    '故障': 'danger',
+    '离线': 'info'
+  };
+  return typeMap[status] || 'info';
+};
+
+// 获取历史记录类型标签
+const getHistoryTypeTag = (type) => {
+  const typeMap = {
+    '状态变更': 'primary',
+    '参数调整': 'warning',
+    '告警处理': 'danger',
+    '维护记录': 'success'
+  };
+  return typeMap[type] || '';
+};
+
+// 格式化时间
+const formatTime = (time) => {
+  if (!time) return '-';
+  const date = new Date(time);
+  return date.toLocaleString('zh-CN');
+};
+
+// 格式化日期
+const formatDate = (date) => {
+  if (!date) return '-';
+  const d = new Date(date);
+  return d.toLocaleDateString('zh-CN');
+};
+
+// 格式化JSON
+const formatJson = (json) => {
+  try {
+    const obj = JSON.parse(json || '{}');
+    return JSON.stringify(obj, null, 2);
+  } catch {
+    return json || '{}';
+  }
+};
+
+// 发送控制命令
+const sendCommand = async (cmd) => {
+  cmd.loading = true;
+  try {
+    // 模拟发送命令
+    await new Promise(resolve => setTimeout(resolve, 1000));
+    ElMessage.success(`${cmd.name}命令执行成功`);
+
+    // 更新历史记录
+    historyData.value.unshift({
+      time: new Date(),
+      type: '设备控制',
+      content: `执行${cmd.name}操作`,
+      operator: '当前用户',
+      result: '成功'
+    });
+  } catch (error) {
+    ElMessage.error(`${cmd.name}命令执行失败`);
+  } finally {
+    cmd.loading = false;
+  }
+};
+
+// 处理模式变更
+const handleModeChange = (value) => {
+  ElMessage.info(`已切换到${value === 'auto' ? '自动' : value === 'manual' ? '手动' : '定时'}模式`);
+};
+
+// 处理功率调节
+const handlePowerChange = (value) => {
+  ElMessage.info(`功率已调节至${value}%`);
 };
 
-getList()
+// 处理温度调节
+const handleTempChange = (value) => {
+  ElMessage.info(`温度已设定为${value}°C`);
+};
+
+// 执行远程控制
+const executeControl = async () => {
+  if (!remoteControlForm.command) {
+    ElMessage.warning('请选择控制指令');
+    return;
+  }
+
+  controlLoading.value = true;
+  try {
+    // 模拟控制执行
+    await new Promise(resolve => setTimeout(resolve, 2000));
+    ElMessage.success('控制指令执行成功');
+    controlDialogOpen.value = false;
+
+    // 刷新数据
+    getList();
+  } catch (error) {
+    ElMessage.error('控制指令执行失败');
+  } finally {
+    controlLoading.value = false;
+  }
+};
+
+// 保存设备信息
+const saveDeviceInfo = () => {
+  ElMessage.success('设备信息保存成功');
+  detailDialogOpen.value = false;
+};
+
+// 3D模型相关方法
+const init3DModel = (device) => {
+  // 这里可以初始化Three.js场景
+  console.log('初始化3D模型:', device.deviceId);
+};
+
+const load3DModel = () => {
+  ElMessage.info('正在加载3D模型...');
+  // 加载3D模型逻辑
+};
+
+const downloadModel = () => {
+  if (detailDevice.value.modelPath) {
+    ElMessage.success('开始下载模型文件');
+    // 下载逻辑
+  } else {
+    ElMessage.warning('暂无模型文件');
+  }
+};
+
+const resetView = () => {
+  ElMessage.info('视角已重置');
+  // 重置3D视角
+};
+
+const toggleRotation = () => {
+  rotationEnabled.value = !rotationEnabled.value;
+  // 切换旋转状态
+};
+
+const toggleWireframe = () => {
+  wireframeMode.value = !wireframeMode.value;
+  ElMessage.info(`已切换到${wireframeMode.value ? '线框' : '实体'}模式`);
+  // 切换渲染模式
+};
+
+const fullscreen3D = () => {
+  const viewer = document.querySelector('.three-d-viewer');
+  if (viewer.requestFullscreen) {
+    viewer.requestFullscreen();
+  }
+};
 </script>
 
 <style scoped>
+.device-management-system {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
 .filter-card {
   margin-bottom: 20px;
-  padding: 20px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.search-form {
+  padding: 10px 0;
+}
+
+/* 统计卡片 */
+.stats-row {
+  margin-bottom: 20px;
+}
+
+.stats-card {
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  transition: all 0.3s;
+}
+
+.stats-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
+}
+
+.stats-card.normal :deep(.el-statistic__number) {
+  color: #67c23a;
+}
+
+.stats-card.warning :deep(.el-statistic__number) {
+  color: #e6a23c;
+}
+
+.stats-card.error :deep(.el-statistic__number) {
+  color: #f56c6c;
 }
 
 .data-table {
-  min-height: 500px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 }
 
-.small-text {
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
+
+/* 位置信息样式 */
+.location-info {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  font-weight: 500;
+}
+
+.location-detail {
   font-size: 12px;
   color: #909399;
+  margin-top: 4px;
+}
+
+/* 状态样式 */
+.status-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  display: inline-block;
+  animation: blink 2s infinite;
+}
+
+.status-normal {
+  background-color: #67c23a;
+}
+
+.status-warning {
+  background-color: #e6a23c;
+}
+
+.status-error {
+  background-color: #f56c6c;
 }
 
-.mt-2 {
-  margin-top: 8px;
+.status-offline {
+  background-color: #909399;
+  animation: none;
+}
+
+@keyframes blink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+.no-alarm {
+  color: #909399;
+  font-size: 12px;
+}
+
+.pagination-container {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+/* 详情弹窗样式 */
+.status-display {
+  padding: 20px;
+}
+
+.status-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 15px;
+  font-size: 14px;
+}
+
+.status-item .label {
+  width: 80px;
+  color: #606266;
+}
+
+.alarm-info {
+  padding: 10px;
+}
+
+.alarm-details {
+  margin-top: 15px;
+  font-size: 14px;
+  line-height: 1.8;
+  color: #606266;
+}
+
+/* 3D模型查看器 */
+.model-viewer {
+  display: grid;
+  grid-template-columns: 2fr 1fr;
+  gap: 20px;
+  height: 500px;
+}
+
+.model-preview {
+  background-color: #1a1a1a;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  overflow: hidden;
+}
+
+.model-info {
+  overflow-y: auto;
+}
+
+.json-display {
+  font-family: 'Consolas', 'Monaco', monospace;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #606266;
+  background-color: #f5f7fa;
+  padding: 10px;
+  border-radius: 4px;
+  overflow: auto;
+  max-height: 200px;
+}
+
+/* 控制面板 */
+.control-panel {
+  padding: 20px;
+}
+
+.control-panel h4 {
+  margin-bottom: 15px;
+  color: #303133;
+}
+
+.control-btn {
+  width: 100%;
+  margin-bottom: 10px;
+}
+
+.unit {
+  margin-left: 10px;
+  color: #909399;
+}
+
+/* 3D查看器 */
+.three-d-viewer {
+  height: 70vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.viewer-toolbar {
+  padding: 10px;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #e4e7ed;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.viewer-container {
+  flex: 1;
+  background-color: #1a1a1a;
+  position: relative;
+  overflow: hidden;
+}
+
+.viewer-container canvas {
+  width: 100%;
+  height: 100%;
+}
+
+.viewer-info {
+  padding: 10px;
+  background-color: #f5f7fa;
+  border-top: 1px solid #e4e7ed;
+}
+
+.mb-4 {
+  margin-bottom: 16px;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+  .stats-row {
+    margin-bottom: 10px;
+  }
+
+  .model-viewer {
+    grid-template-columns: 1fr;
+    height: auto;
+  }
+
+  .model-preview {
+    height: 300px;
+  }
+}
+
+/* 暗色主题支持 */
+@media (prefers-color-scheme: dark) {
+  .device-management-system {
+    background-color: #1a1a1a;
+  }
+
+  .json-display {
+    background-color: #2a2a2a;
+    color: #e0e0e0;
+  }
 }
 </style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 758 - 158
pm_ui/src/views/buildingEquipmentMonitoring/index2.vue


+ 822 - 134
pm_ui/src/views/buildingEquipmentMonitoring/index3.vue

@@ -9,7 +9,7 @@
         </el-form-item>
 
         <el-form-item label="层级">
-          <el-select v-model="queryParams.level" placeholder="请选择层级" style="width: 180px;">
+          <el-select v-model="queryParams.level" placeholder="请选择层级" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="一级区域" value="1" />
             <el-option label="二级区域" value="2" />
@@ -19,7 +19,7 @@
 
         <!-- 设备统计条件 -->
         <el-form-item label="设备类型">
-          <el-select v-model="queryParams.deviceType" placeholder="请选择设备类型" style="width: 180px;">
+          <el-select v-model="queryParams.deviceType" placeholder="请选择设备类型" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="控制器" value="CONTROLLER" />
             <el-option label="传感器" value="SENSOR" />
@@ -31,161 +31,425 @@
 
         <!-- 状态过滤条件 -->
         <el-form-item label="设备状态">
-          <el-select v-model="queryParams.status" placeholder="请选择设备状态" style="width: 180px;">
+          <el-select v-model="queryParams.status" placeholder="请选择设备状态" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="在线" value="在线" />
+            <el-option label="离线" value="离线" />
             <el-option label="故障" value="故障" />
           </el-select>
         </el-form-item>
 
         <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
-          <el-button icon="Refresh" @click="resetSearch">重置</el-button>
+          <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
+          <el-button :icon="Refresh" @click="resetSearch">重置</el-button>
+          <el-button type="success" :icon="Download" @click="handleExport">导出</el-button>
         </el-form-item>
       </el-form>
     </el-card>
 
+    <!-- 统计卡片 -->
+    <el-row :gutter="20" class="stats-row">
+      <el-col :span="6">
+        <el-card class="stats-card">
+          <el-statistic title="区域总数" :value="stats.totalAreas">
+            <template #prefix>
+              <el-icon><Location /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card">
+          <el-statistic title="设备总数" :value="stats.totalDevices">
+            <template #prefix>
+              <el-icon><Monitor /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card online-card">
+          <el-statistic title="在线设备" :value="stats.onlineDevices">
+            <template #prefix>
+              <el-icon><CircleCheck /></el-icon>
+            </template>
+            <template #suffix>
+              <span class="rate-text">({{ stats.onlineRate }}%)</span>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card health-card">
+          <el-statistic title="平均健康指数" :value="stats.avgHealthIndex" suffix="分">
+            <template #prefix>
+              <el-icon><TrendCharts /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+    </el-row>
+
     <!-- 数据表格区域 -->
     <el-card class="data-table">
       <el-table
           :data="areaList"
           border
-          size="small"
           v-loading="isLoading"
           row-key="areaId"
           @row-click="handleRowClick"
+          :row-class-name="tableRowClassName"
+
       >
-        <el-table-column label="区域ID" prop="areaId" align="center"/>
-        <el-table-column label="区域名称" prop="areaName" align="center"/>
+        <el-table-column label="区域ID" prop="areaId" align="center" width="120" fixed="left" />
+        <el-table-column label="区域名称" prop="areaName" align="center" min-width="150">
+          <template #default="scope">
+            <div class="area-name-cell">
+              <span class="level-indicator" :class="`level-${scope.row.level}`"></span>
+              {{ scope.row.areaName }}
+            </div>
+          </template>
+        </el-table-column>
         <el-table-column label="层级" prop="level" align="center" width="100">
           <template #default="scope">
-            {{ scope.row.level === 1 ? '一级' : scope.row.level === 2 ? '二级' : '三级' }}
+            <el-tag :type="getLevelType(scope.row.level)" size="small">
+              {{ scope.row.level === 1 ? '一级' : scope.row.level === 2 ? '二级' : '三级' }}
+            </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="设备总数" align="center" width="100">
+        <el-table-column label="设备统计" align="center" width="300">
           <template #default="scope">
-            {{ scope.row.deviceStats.reduce((sum, stat) => sum + stat.count, 0) }}台
+            <div class="device-stats">
+              <el-tooltip
+                  v-for="stat in scope.row.deviceStats"
+                  :key="stat.deviceType"
+                  :content="`${mapDeviceType(stat.deviceType)}: ${stat.count}台 (在线率: ${stat.onlineRate}%)`"
+                  placement="top"
+              >
+                <div class="device-stat-item">
+                  <el-icon :style="{color: getDeviceTypeColor(stat.deviceType)}">
+                    <component :is="getDeviceIcon(stat.deviceType)" />
+                  </el-icon>
+                  <span>{{ stat.count }}</span>
+                </div>
+              </el-tooltip>
+            </div>
           </template>
         </el-table-column>
-        <el-table-column label="平均在线率" align="center" width="120">
+        <el-table-column label="设备总数" align="center" width="100" sortable>
           <template #default="scope">
-            {{ getAverageOnlineRate(scope.row.deviceStats).toFixed(1) }}%
+            <el-badge :value="getTotalDeviceCount(scope.row.deviceStats)" class="device-count-badge">
+              <el-icon><Monitor /></el-icon>
+            </el-badge>
           </template>
         </el-table-column>
-        <el-table-column label="健康指数" align="center" width="160">
+        <el-table-column label="平均在线率" align="center" width="120" sortable>
           <template #default="scope">
-            <div>
-              <span>{{ parseKeyMetrics(scope.row.keyMetrics).healthIndex }} 分</span>
+            <div class="online-rate">
               <el-progress
-                  :percentage="parseKeyMetrics(scope.row.keyMetrics).healthIndex"
-                  type="line"
-                  :status="getHealthStatus(parseKeyMetrics(scope.row.keyMetrics).healthIndex)"
-                  style="width: 120px; margin-top: 4px;"
+                  :percentage="getAverageOnlineRate(scope.row.deviceStats)"
+                  :width="60"
+                  type="circle"
+                  :color="getOnlineRateColor(getAverageOnlineRate(scope.row.deviceStats))"
+              >
+                <template #default="{percentage}">
+                  <span class="percentage-text">{{ percentage.toFixed(0) }}%</span>
+                </template>
+              </el-progress>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="健康指数" align="center" width="160" sortable>
+          <template #default="scope">
+            <div class="health-index">
+              <span class="health-score">{{ parseKeyMetrics(scope.row.keyMetrics).healthIndex }}</span>
+              <el-progress
+                  :percentage="Number(parseKeyMetrics(scope.row.keyMetrics).healthIndex)"
+                  :stroke-width="6"
+                  :color="getHealthColor(Number(parseKeyMetrics(scope.row.keyMetrics).healthIndex))"
               />
             </div>
           </template>
         </el-table-column>
+        <el-table-column label="操作" align="center" width="150" fixed="right">
+          <template #default="scope">
+            <el-button type="primary" size="small" link @click.stop="handleViewDetail(scope.row)">
+              详情
+            </el-button>
+            <el-button type="warning" size="small" link @click.stop="handleViewChart(scope.row)">
+              图表
+            </el-button>
+            <el-button type="success" size="small" link @click.stop="handleViewMap(scope.row)">
+              地图
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table>
 
       <!-- 分页组件 -->
-      <pagination
+      <el-pagination
           v-show="total > 0"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNum"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+          class="pagination"
       />
     </el-card>
 
     <!-- 详情弹窗 -->
-    <el-dialog :visible.sync="detailDialogOpen" title="区域详情" width="1000px">
-      <el-descriptions :column="2" border>
-        <el-descriptions-item label="区域ID" span="1">{{ detailArea.areaId }}</el-descriptions-item>
-        <el-descriptions-item label="区域名称" span="1">{{ detailArea.areaName }}</el-descriptions-item>
-        <el-descriptions-item label="父区域" span="1">
-          {{ detailArea.parentAreaId || "无" }}
+    <el-dialog v-model="detailDialogOpen" title="区域详情" width="1200px" top="5vh">
+      <el-descriptions :column="3" border>
+        <el-descriptions-item label="区域ID">{{ detailArea.areaId }}</el-descriptions-item>
+        <el-descriptions-item label="区域名称">{{ detailArea.areaName }}</el-descriptions-item>
+        <el-descriptions-item label="父区域">
+          <el-tag v-if="detailArea.parentAreaId" type="info">{{ detailArea.parentAreaId }}</el-tag>
+          <span v-else>无</span>
         </el-descriptions-item>
-        <el-descriptions-item label="层级" span="1">
-          {{ detailArea.level === 1 ? '一级区域' : detailArea.level === 2 ? '二级区域' : '三级区域' }}
+        <el-descriptions-item label="层级">
+          <el-tag :type="getLevelType(detailArea.level)">
+            {{ detailArea.level === 1 ? '一级区域' : detailArea.level === 2 ? '二级区域' : '三级区域' }}
+          </el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="关键指标" span="2">
-          <div>平均能耗:{{ getKeyMetric(detailArea.keyMetrics, 0) }} kWh</div>
-          <div>最大负载:{{ getKeyMetric(detailArea.keyMetrics, 1) }} W</div>
-          <div>健康指数:{{ getKeyMetric(detailArea.keyMetrics, 2) }}分</div>
+        <el-descriptions-item label="平均能耗" :span="1">
+          <el-statistic :value="parseKeyMetrics(detailArea.keyMetrics).averageEnergy" suffix="kWh">
+            <template #prefix>
+              <el-icon><Lightning /></el-icon>
+            </template>
+          </el-statistic>
+        </el-descriptions-item>
+        <el-descriptions-item label="最大负载" :span="1">
+          <el-statistic :value="parseKeyMetrics(detailArea.keyMetrics).maxLoad" suffix="W">
+            <template #prefix>
+              <el-icon><Connection /></el-icon>
+            </template>
+          </el-statistic>
         </el-descriptions-item>
       </el-descriptions>
 
       <!-- 设备统计图表 -->
-      <el-card class="mt-4" title="设备类型分布">
-        <el-chart :height="300">
-          <el-pie-chart
-              :data="chartData"
-              :options="pieOptions"
-          />
-        </el-chart>
-      </el-card>
+      <el-row :gutter="20" class="mt-4">
+        <el-col :span="12">
+          <el-card>
+            <template #header>
+              <span>设备类型分布</span>
+            </template>
+            <div ref="pieChartRef" style="height: 300px;"></div>
+          </el-card>
+        </el-col>
+        <el-col :span="12">
+          <el-card>
+            <template #header>
+              <span>设备在线率趋势</span>
+            </template>
+            <div ref="lineChartRef" style="height: 300px;"></div>
+          </el-card>
+        </el-col>
+      </el-row>
 
       <!-- 空间分布热力图 -->
-      <el-card class="mt-4" title="设备位置热力图">
-        <div class="heatmap-container">
+      <el-card class="mt-4">
+        <template #header>
+          <span>设备位置热力图</span>
+        </template>
+        <div class="heatmap-container" ref="heatmapContainer">
           <div
-              v-for="(point, index) in heatmapPoints"
-              :key="index"
+              v-for="point in heatmapPoints"
+              :key="point.id"
               class="heatmap-point"
               :style="{
-              left: point.x + 'px',
-              top: point.y + 'px',
-              width: point.size * 2 + 'px',
-              height: point.size * 2 + 'px',
-              background: `rgba(255, 165, 0, ${point.opacity})`
-            }"
-          />
+                left: point.x + 'px',
+                top: point.y + 'px',
+                width: point.size + 'px',
+                height: point.size + 'px',
+                background: `radial-gradient(circle, rgba(255, 87, 34, ${point.opacity}) 0%, transparent 70%)`
+              }"
+              @click="handlePointClick(point)"
+          >
+            <el-tooltip :content="`设备密度: ${point.density}`" placement="top">
+              <div class="point-inner"></div>
+            </el-tooltip>
+          </div>
         </div>
       </el-card>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="detailDialogOpen = false">关闭</el-button>
+          <el-button type="primary" @click="handlePrintDetail">打印</el-button>
+        </span>
+      </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue';
-import { ElMessage, ElProgress } from 'element-plus';
-import { getBuildingEquipmentMonitoringAreaList } from '@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring';
+import { ref, reactive, computed, onMounted, nextTick } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+  Search,
+  Refresh,
+  Download,
+  Location,
+  Monitor,
+  CircleCheck,
+  TrendCharts,
+  Lightning,
+  Connection,
+  Cpu,
+  Camera,
+  Microphone,
+  VideoCamera,
+  DataAnalysis
+} from '@element-plus/icons-vue';
+import * as echarts from 'echarts';
+import { getBuildingEquipmentMonitoringAreaList, getAreaStats } from '@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring';
 
 // 查询参数
 const queryParams = reactive({
   areaName: '',
   level: '',
-  deviceType: [],
-  status: [],
+  deviceType: '',
+  status: '',
   pageNum: 1,
   pageSize: 10,
 });
 
-// 表格数据
+// 数据状态
 const areaList = ref([]);
 const total = ref(0);
 const isLoading = ref(false);
 const detailDialogOpen = ref(false);
 const detailArea = ref({});
 
-// 图表数据
-const chartData = ref([]);
-const pieOptions = computed(() => ({
-  title: { text: '设备类型占比' },
-  legend: { orient: 'vertical', left: 'left' },
-  series: [{ type: 'pie', radius: '50%', data: chartData.value }],
-}));
+// 统计数据
+const stats = reactive({
+  totalAreas: 0,
+  totalDevices: 0,
+  onlineDevices: 0,
+  onlineRate: 0,
+  avgHealthIndex: 0
+});
+
+// 图表实例
+const pieChartRef = ref(null);
+const lineChartRef = ref(null);
+let pieChart = null;
+let lineChart = null;
 
 // 热力图数据
 const heatmapPoints = ref([]);
+const heatmapContainer = ref(null);
+
+// 获取层级标签类型
+const getLevelType = (level) => {
+  const types = ['success', 'warning', 'danger'];
+  return types[level - 1] || 'info';
+};
+
+// 获取设备图标
+const getDeviceIcon = (type) => {
+  const icons = {
+    'CONTROLLER': Cpu,
+    'SENSOR': DataAnalysis,
+    'ACTUATOR': Connection,
+    'BROADCASTER': Microphone,
+    'MONITOR': Camera
+  };
+  return icons[type] || Monitor;
+};
+
+// 设备类型映射
+const mapDeviceType = (type) => {
+  const typeMap = {
+    'CONTROLLER': '控制器',
+    'SENSOR': '传感器',
+    'ACTUATOR': '执行器',
+    'BROADCASTER': '广播设备',
+    'MONITOR': '监控设备',
+  };
+  return typeMap[type] || type;
+};
+
+// 设备类型颜色
+const getDeviceTypeColor = (type) => {
+  const colorMap = {
+    'CONTROLLER': '#409eff',
+    'SENSOR': '#67c23a',
+    'ACTUATOR': '#f56c6c',
+    'BROADCASTER': '#e6a23c',
+    'MONITOR': '#909399',
+  };
+  return colorMap[type] || '#409eff';
+};
+
+// 计算设备总数
+const getTotalDeviceCount = (stats) => {
+  return stats.reduce((sum, stat) => sum + stat.count, 0);
+};
+
+// 计算平均在线率
+const getAverageOnlineRate = (stats) => {
+  if (stats.length === 0) return 0;
+  const totalDevices = stats.reduce((sum, stat) => sum + stat.count, 0);
+  const onlineDevices = stats.reduce((sum, stat) => sum + (stat.count * stat.onlineRate / 100), 0);
+  return totalDevices > 0 ? (onlineDevices / totalDevices) * 100 : 0;
+};
+
+// 获取在线率颜色
+const getOnlineRateColor = (rate) => {
+  if (rate >= 90) return '#67c23a';
+  if (rate >= 70) return '#e6a23c';
+  return '#f56c6c';
+};
+
+// 解析关键指标
+const parseKeyMetrics = (keyMetricsStr) => {
+  if (!keyMetricsStr) return { averageEnergy: 0, maxLoad: 0, healthIndex: 0 };
+  const [averageEnergy = 0, maxLoad = 0, healthIndex = 0] = keyMetricsStr.split(',').map(Number);
+  return {
+    averageEnergy: averageEnergy.toFixed(2),
+    maxLoad: maxLoad.toFixed(0),
+    healthIndex: healthIndex.toFixed(0),
+  };
+};
+
+// 获取健康指数颜色
+const getHealthColor = (healthIndex) => {
+  if (healthIndex >= 90) return '#67c23a';
+  if (healthIndex >= 70) return '#409eff';
+  if (healthIndex >= 50) return '#e6a23c';
+  return '#f56c6c';
+};
+
+// 表格行样式
+const tableRowClassName = ({ row }) => {
+  const healthIndex = Number(parseKeyMetrics(row.keyMetrics).healthIndex);
+  if (healthIndex < 60) return 'warning-row';
+  if (healthIndex < 40) return 'danger-row';
+  return '';
+};
+
+// 获取统计数据
+const getStats = async () => {
+  try {
+    const response = await getAreaStats();
+    Object.assign(stats, response.data);
+  } catch (error) {
+    console.error('获取统计数据失败:', error);
+  }
+};
 
 // 获取数据
 const getList = async () => {
   isLoading.value = true;
   try {
     const response = await getBuildingEquipmentMonitoringAreaList(queryParams);
-    areaList.value = response.data.list;
-    total.value = response.data.total;
+    areaList.value = response.data.list || [];
+    total.value = response.data.total || 0;
   } catch (error) {
     ElMessage.error('获取区域数据失败');
     console.error(error);
@@ -198,16 +462,34 @@ const getList = async () => {
 const handleSearch = () => {
   queryParams.pageNum = 1;
   getList();
+  getStats();
 };
 
 // 重置
 const resetSearch = () => {
-  queryParams.areaName = '';
-  queryParams.level = '';
-  queryParams.deviceType = [];
-  queryParams.status = [];
-  queryParams.pageNum = 1;
+  Object.assign(queryParams, {
+    areaName: '',
+    level: '',
+    deviceType: '',
+    status: '',
+    pageNum: 1,
+    pageSize: 10,
+  });
   getList();
+  getStats();
+};
+
+// 导出数据
+const handleExport = async () => {
+  try {
+    isLoading.value = true;
+    await exportAreaData(queryParams);
+    ElMessage.success('导出成功');
+  } catch (error) {
+    ElMessage.error('导出失败');
+  } finally {
+    isLoading.value = false;
+  }
 };
 
 // 分页
@@ -222,115 +504,521 @@ const handleSizeChange = (newSize) => {
   getList();
 };
 
-// 行点击事件(打开详情弹窗)
-const handleRowClick = (row) => {
+// 查看详情
+const handleViewDetail = (row) => {
   detailArea.value = { ...row };
-  // 解析设备统计数据
-  chartData.value = row.deviceStats.map(stat => ({
-    name: mapDeviceType(stat.deviceType),
-    value: stat.count,
-    itemStyle: { color: getDeviceTypeColor(stat.deviceType) }
-  }));
-  // 解析热力图数据
-  parseHeatmapData(row.heatmapData);
   detailDialogOpen.value = true;
+  nextTick(() => {
+    initCharts(row);
+    parseHeatmapData(row.heatmapData);
+  });
 };
 
-// 设备类型映射
-const mapDeviceType = (type) => {
-  return {
-    'CONTROLLER': '控制器',
-    'SENSOR': '传感器',
-    'ACTUATOR': '执行器',
-    'BROADCASTER': '广播设备',
-    'MONITOR': '监控设备',
-  }[type] || type;
+// 行点击事件
+const handleRowClick = (row) => {
+  handleViewDetail(row);
 };
 
-// 设备类型颜色
-const getDeviceTypeColor = (type) => {
-  return {
-    'CONTROLLER': '#409eff',
-    'SENSOR': '#67c23a',
-    'ACTUATOR': '#f56c6c',
-    'BROADCASTER': '#e6a23c',
-    'MONITOR': '#909399',
-  }[type] || '#409eff';
+// 查看图表
+const handleViewChart = (row) => {
+  // 可以打开单独的图表弹窗
+  ElMessage.info('图表功能开发中...');
 };
 
-// 计算平均在线率
-const getAverageOnlineRate = (stats) => {
-  if (stats.length === 0) return 0;
-  return stats.reduce((sum, stat) => sum + stat.onlineRate, 0) / stats.length;
+// 查看地图
+const handleViewMap = (row) => {
+  // 可以打开地图视图
+  ElMessage.info('地图功能开发中...');
 };
 
-// 解析关键指标(平均能耗,最大负载,健康指数)
-const parseKeyMetrics = (keyMetricsStr) => {
-  const [averageEnergy, maxLoad, healthIndex] = keyMetricsStr.split(',').map(Number);
-  return {
-    averageEnergy: averageEnergy.toFixed(2), // 平均能耗保留两位小数
-    maxLoad: maxLoad.toFixed(0), // 最大负载取整
-    healthIndex: healthIndex.toFixed(0), // 健康指数取整
-  };
-};
+// 初始化图表
+const initCharts = (areaData) => {
+  // 销毁旧实例
+  if (pieChart) pieChart.dispose();
+  if (lineChart) lineChart.dispose();
 
-// 获取健康指数进度条状态
-const getHealthStatus = (healthIndex) => {
-  if (healthIndex >= 90) return 'success'; // 绿色
-  if (healthIndex >= 80) return 'warning'; // 黄色
-  return 'danger'; // 红色
+  // 饼图 - 设备类型分布
+  if (pieChartRef.value) {
+    pieChart = echarts.init(pieChartRef.value);
+    const pieData = areaData.deviceStats.map(stat => ({
+      name: mapDeviceType(stat.deviceType),
+      value: stat.count,
+      itemStyle: { color: getDeviceTypeColor(stat.deviceType) }
+    }));
+
+    const pieOption = {
+      tooltip: {
+        trigger: 'item',
+        formatter: '{a} <br/>{b}: {c} ({d}%)'
+      },
+      legend: {
+        orient: 'vertical',
+        left: 'left',
+        data: pieData.map(item => item.name)
+      },
+      series: [
+        {
+          name: '设备类型',
+          type: 'pie',
+          radius: ['40%', '70%'],
+          avoidLabelOverlap: false,
+          itemStyle: {
+            borderRadius: 10,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: {
+            show: false,
+            position: 'center'
+          },
+          emphasis: {
+            label: {
+              show: true,
+              fontSize: '20',
+              fontWeight: 'bold'
+            }
+          },
+          labelLine: {
+            show: false
+          },
+          data: pieData
+        }
+      ]
+    };
+    pieChart.setOption(pieOption);
+  }
+
+  // 折线图 - 在线率趋势(模拟数据)
+  if (lineChartRef.value) {
+    lineChart = echarts.init(lineChartRef.value);
+    const hours = Array.from({ length: 24 }, (_, i) => `${i}:00`);
+    const onlineRates = hours.map(() => 80 + Math.random() * 20);
+
+    const lineOption = {
+      tooltip: {
+        trigger: 'axis',
+        formatter: '{b}<br/>在线率: {c}%'
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: hours
+      },
+      yAxis: {
+        type: 'value',
+        min: 0,
+        max: 100,
+        axisLabel: {
+          formatter: '{value}%'
+        }
+      },
+      series: [
+        {
+          name: '在线率',
+          type: 'line',
+          smooth: true,
+          symbol: 'none',
+          lineStyle: {
+            width: 3,
+            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+              { offset: 0, color: '#409eff' },
+              { offset: 1, color: '#67c23a' }
+            ])
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
+              { offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
+            ])
+          },
+          data: onlineRates
+        }
+      ]
+    };
+    lineChart.setOption(lineOption);
+  }
 };
 
 // 解析热力图数据
 const parseHeatmapData = (heatmapStr) => {
   try {
+    if (!heatmapStr) {
+      // 生成模拟数据
+      heatmapPoints.value = Array.from({ length: 20 }, (_, i) => ({
+        id: `point-${i}`,
+        x: Math.random() * 500,
+        y: Math.random() * 300,
+        size: 20 + Math.random() * 40,
+        opacity: 0.3 + Math.random() * 0.7,
+        density: Math.floor(Math.random() * 10)
+      }));
+      return;
+    }
+
     const { heatData } = JSON.parse(heatmapStr);
     heatmapPoints.value = heatData.map(([x, y, opacity], index) => ({
       id: `point-${index}`,
-      x: x * 2 + 50, // 模拟坐标偏移
-      y: y * 2 + 50,
-      size: opacity * 20, // 气泡大小
-      opacity: opacity // 透明度
+      x: x * 5,
+      y: y * 3,
+      size: 20 + opacity * 40,
+      opacity: opacity,
+      density: Math.floor(opacity * 10)
     }));
   } catch (error) {
+    console.error('解析热力图数据失败:', error);
     heatmapPoints.value = [];
   }
 };
 
-// 初始加载数据
-getList();
+// 点击热力图点
+const handlePointClick = (point) => {
+  ElMessage.info(`设备密度: ${point.density}`);
+};
+
+// 打印详情
+const handlePrintDetail = () => {
+  window.print();
+};
+
+// 窗口大小改变时重绘图表
+const handleResize = () => {
+  if (pieChart) pieChart.resize();
+  if (lineChart) lineChart.resize();
+};
+
+// 初始化
+onMounted(() => {
+  getList();
+  getStats();
+  window.addEventListener('resize', handleResize);
+});
+
+// 清理
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
+  if (pieChart) pieChart.dispose();
+  if (lineChart) lineChart.dispose();
+});
 </script>
 
 <style scoped>
+.area-management-system {
+  padding: 20px;
+}
+
 .filter-card {
   margin-bottom: 20px;
-  padding: 20px;
+}
+
+.search-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.stats-row {
+  margin-bottom: 20px;
+}
+
+.stats-card {
+  text-align: center;
+  transition: all 0.3s;
+}
+
+.stats-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.online-card :deep(.el-statistic__number) {
+  color: #67c23a;
+}
+
+.health-card :deep(.el-statistic__number) {
+  color: #409eff;
+}
+
+.rate-text {
+  font-size: 14px;
+  color: #909399;
+  margin-left: 5px;
 }
 
 .data-table {
   min-height: 500px;
 }
 
-.el-chart {
-  height: 300px;
+.area-name-cell {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.level-indicator {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.level-1 { background-color: #67c23a; }
+.level-2 { background-color: #e6a23c; }
+.level-3 { background-color: #f56c6c; }
+
+.device-stats {
+  display: flex;
+  justify-content: center;
+  gap: 15px;
+}
+
+.device-stat-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 8px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.device-stat-item:hover {
+  background: #e6e8eb;
+  transform: scale(1.05);
+}
+
+.device-count-badge {
+  cursor: pointer;
+}
+
+.online-rate {
+  display: flex;
+  justify-content: center;
+}
+
+.percentage-text {
+  font-size: 14px;
+  font-weight: bold;
+}
+
+.health-index {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 5px;
+}
+
+.health-score {
+  font-size: 18px;
+  font-weight: bold;
+  color: #303133;
+}
+
+:deep(.warning-row) {
+  background-color: #fef0f0;
+}
+
+:deep(.danger-row) {
+  background-color: #fde2e2;
+}
+
+.mt-4 {
+  margin-top: 20px;
 }
 
 .heatmap-container {
   position: relative;
   height: 400px;
-  border: 1px solid #ebeef5;
-  background-color: #f5f7fa;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%);
+  border-radius: 8px;
   overflow: hidden;
 }
 
 .heatmap-point {
   position: absolute;
   border-radius: 50%;
-  transition: all 0.2s;
+  cursor: pointer;
+  transition: all 0.3s;
+  animation: pulse 2s infinite;
 }
 
 .heatmap-point:hover {
-  opacity: 1;
   transform: scale(1.2);
+  z-index: 10;
+}
+
+.point-inner {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.05);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+/* 响应式布局 */
+@media (max-width: 1200px) {
+  .stats-row .el-col {
+    margin-bottom: 10px;
+  }
+
+  .device-stats {
+    flex-wrap: wrap;
+  }
+}
+
+@media (max-width: 768px) {
+  .search-form {
+    display: block;
+  }
+
+  .search-form .el-form-item {
+    margin-bottom: 10px;
+  }
+
+  .el-dialog {
+    width: 95% !important;
+  }
+
+  .el-table {
+    font-size: 12px;
+  }
+
+  .device-stat-item {
+    padding: 2px 4px;
+    font-size: 12px;
+  }
+}
+
+/* 打印样式 */
+@media print {
+  .filter-card,
+  .stats-row,
+  .pagination,
+  .dialog-footer,
+  .el-table__column:last-child {
+    display: none !important;
+  }
+
+  .data-table {
+    box-shadow: none !important;
+  }
+
+  .heatmap-point {
+    animation: none !important;
+  }
+}
+
+/* 自定义滚动条 */
+.el-table__body-wrapper::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.el-table__body-wrapper::-webkit-scrollbar-track {
+  background: #f1f1f1;
+}
+
+.el-table__body-wrapper::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 4px;
+}
+
+.el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
+  background: #909399;
+}
+
+/* 加载动画 */
+.el-loading-mask {
+  background-color: rgba(255, 255, 255, 0.9);
+}
+/* 修复Badge数字显示不全的问题 */
+:deep(.el-table__row) {
+  /* 确保行有足够的高度容纳Badge */
+  min-height: 55px;
+}
+
+:deep(.el-table__cell) {
+  /* 允许内容溢出单元格 */
+  overflow: visible !important;
+}
+
+/* 设备总数单元格特殊处理 */
+.total-device-count {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  /* 给Badge留出足够的空间 */
+  padding-top: 10px;
+  padding-bottom: 5px;
+}
+
+/* Badge容器调整 */
+.device-count-badge {
+  display: inline-flex;
+  position: relative;
+}
+
+/* 确保Badge内容不被裁剪 */
+.device-count-badge :deep(.el-badge__content) {
+  /* 调整Badge位置,确保在单元格内完全显示 */
+  top: -5px !important;
+  right: 2px !important;
+}
+
+/* 针对设备总数这一列的特殊处理 */
+:deep(.el-table__body td:nth-child(5)) {
+  /* 第5列是设备总数列,根据实际情况调整序号 */
+  overflow: visible !important;
+  position: relative;
+}
+
+:deep(.el-table__body td:nth-child(5) .cell) {
+  overflow: visible !important;
+  /* 增加上内边距,为Badge留出空间 */
+  /*padding-top: 2px !important;*/
+}
+
+/* 如果上面的方案还不够,可以尝试这个更激进的方案 */
+:deep(.el-table) {
+  /* 允许表格内容溢出 */
+  overflow: visible !important;
+}
+
+:deep(.el-table__body-wrapper) {
+  overflow-x: auto !important;
+  overflow-y: visible !important;
+}
+
+:deep(.el-table__body) {
+  overflow: visible !important;
 }
 </style>

+ 692 - 138
pm_ui/src/views/buildingEquipmentMonitoring/index4.vue

@@ -9,7 +9,7 @@
         </el-form-item>
 
         <el-form-item label="指令类型">
-          <el-select v-model="queryParams.cmdType" placeholder="请选择指令类型" style="width: 180px;">
+          <el-select v-model="queryParams.cmdType" placeholder="请选择指令类型" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="参数设置" value="SET_PARAM" />
             <el-option label="报警触发" value="TRIGGER_ALARM" />
@@ -21,11 +21,14 @@
 
         <!-- 执行状态条件 -->
         <el-form-item label="指令状态">
-          <el-select v-model="queryParams.status" placeholder="请选择状态" style="width: 180px;">
+          <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="待执行" value="PENDING" />
+            <el-option label="执行中" value="PROCESSING" />
             <el-option label="成功" value="SUCCESS" />
             <el-option label="失败" value="FAILED" />
+            <el-option label="重试中" value="RETRYING" />
+            <el-option label="已取消" value="CANCELED" />
           </el-select>
         </el-form-item>
 
@@ -36,7 +39,7 @@
 
         <!-- 响应监控条件 -->
         <el-form-item label="设备反馈">
-          <el-select v-model="queryParams.feedbackStatus" placeholder="请选择反馈状态" style="width: 180px;">
+          <el-select v-model="queryParams.feedbackStatus" placeholder="请选择反馈状态" clearable style="width: 180px;">
             <el-option label="全部" value="" />
             <el-option label="正常" value="OK" />
             <el-option label="超时" value="TIMEOUT" />
@@ -44,87 +47,200 @@
           </el-select>
         </el-form-item>
 
+        <!-- 时间范围选择 -->
+        <el-form-item label="执行时间">
+          <el-date-picker
+              v-model="dateRange"
+              type="datetimerange"
+              range-separator="至"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              @change="handleDateChange"
+          />
+        </el-form-item>
+
         <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
-          <el-button icon="Refresh" @click="resetSearch">重置</el-button>
+          <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
+          <el-button :icon="Refresh" @click="resetSearch">重置</el-button>
+          <el-button type="success" :icon="Download" @click="handleExport">导出</el-button>
         </el-form-item>
       </el-form>
     </el-card>
 
+    <!-- 统计卡片 -->
+    <el-row :gutter="20" class="stats-row">
+      <el-col :span="6">
+        <el-card class="stats-card">
+          <el-statistic title="今日指令总数" :value="stats.todayTotal">
+            <template #prefix>
+              <el-icon><DocumentCopy /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card success-card">
+          <el-statistic title="执行成功" :value="stats.successCount">
+            <template #prefix>
+              <el-icon><CircleCheck /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card error-card">
+          <el-statistic title="执行失败" :value="stats.failedCount">
+            <template #prefix>
+              <el-icon><CircleClose /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stats-card">
+          <el-statistic title="平均响应时间" :value="stats.avgResponseTime" suffix="ms">
+            <template #prefix>
+              <el-icon><Timer /></el-icon>
+            </template>
+          </el-statistic>
+        </el-card>
+      </el-col>
+    </el-row>
+
     <!-- 数据表格区域 -->
     <el-card class="data-table">
       <el-table
           :data="commandList"
           border
-          size="small"
           v-loading="isLoading"
           row-key="cmdId"
           @row-click="handleRowClick"
+          :row-class-name="tableRowClassName"
       >
-        <el-table-column label="指令ID" prop="cmdId" align="center" width="140" />
-        <el-table-column label="目标设备" align="center" >
+        <el-table-column label="指令ID" prop="cmdId" align="center" width="140" fixed="left" />
+        <el-table-column label="目标设备" align="center" min-width="200">
+          <template #default="scope">
+            <div class="device-tags">
+              <el-tag
+                  v-for="device in scope.row.targetDevices"
+                  :key="device"
+                  size="small"
+                  type="info"
+                  class="device-tag"
+              >
+                {{ device }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="指令类型" prop="cmdType" align="center" width="120">
+          <template #default="scope">
+            <el-tag :type="getCmdTypeColor(scope.row.cmdType)">
+              {{ mapCmdType(scope.row.cmdType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="指令状态" prop="status" align="center" width="120">
           <template #default="scope">
             <el-tag
-                v-for="device in scope.row.targetDevices"
-                :key="device"
-                size="small"
-                type="info"
+                :type="getStatusColor(scope.row.status)"
+                :effect="scope.row.status === 'PROCESSING' ? 'dark' : 'light'"
             >
-              {{ device }}
+              <span v-if="scope.row.status === 'PROCESSING'" class="processing-dot"></span>
+              {{ mapStatus(scope.row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="设备反馈" align="center" width="120">
+          <template #default="scope">
+            <el-tag :type="getFeedbackColor(scope.row.feedbackStatus)" size="small">
+              {{ mapFeedbackStatus(scope.row.feedbackStatus) }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="指令类型" prop="cmdType" align="center" />
-        <el-table-column label="指令状态" prop="status" align="center" width="120">
-        <template #default="scope">
-          <el-tag
-              :type="getStatusColor(scope.row.status)"
-              :effect="scope.row.status === 'SUCCESS' ? 'light' : 'dark'"
-          >
-            {{ mapStatus(scope.row.status) }}
-          </el-tag>
-        </template>
-      </el-table-column>
         <el-table-column label="操作员" prop="operatorId" align="center" width="100" />
-        <el-table-column label="执行时间" prop="timestamp" align="center" width="180" />
-        <el-table-column label="执行耗时" align="center" width="120">
+        <el-table-column label="执行时间" prop="timestamp" align="center" width="180" sortable />
+        <el-table-column label="执行耗时" align="center" width="120" sortable>
+          <template #default="scope">
+            <span :class="getTimeClass(scope.row.executionTime)">
+              {{ scope.row.executionTime }} ms
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150" fixed="right">
           <template #default="scope">
-            {{ scope.row.executionTime }} ms
+            <el-button
+                v-if="scope.row.status === 'FAILED'"
+                type="primary"
+                size="small"
+                link
+                @click.stop="handleRetry(scope.row)"
+            >
+              重试
+            </el-button>
+            <el-button
+                v-if="scope.row.status === 'PENDING' || scope.row.status === 'PROCESSING'"
+                type="danger"
+                size="small"
+                link
+                @click.stop="handleCancel(scope.row)"
+            >
+              取消
+            </el-button>
+            <el-button
+                type="info"
+                size="small"
+                link
+                @click.stop="handleViewDetail(scope.row)"
+            >
+              详情
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
 
       <!-- 分页组件 -->
-      <pagination
+      <el-pagination
           v-show="total > 0"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
           :total="total"
-          v-model:page="queryParams.pageNum"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+          class="pagination"
       />
     </el-card>
 
     <!-- 详情弹窗 -->
-    <el-dialog :visible.sync="detailDialogOpen" title="指令详情" width="1000px">
+    <el-dialog v-model="detailDialogOpen" title="指令详情" width="1000px" top="5vh">
       <el-descriptions :column="2" border>
         <el-descriptions-item label="指令ID" span="1">{{ detailCommand.cmdId }}</el-descriptions-item>
-        <el-descriptions-item label="指令类型" span="1">{{ mapCmdType(detailCommand.cmdType) }}</el-descriptions-item>
+        <el-descriptions-item label="指令类型" span="1">
+          <el-tag :type="getCmdTypeColor(detailCommand.cmdType)">
+            {{ mapCmdType(detailCommand.cmdType) }}
+          </el-tag>
+        </el-descriptions-item>
         <el-descriptions-item label="目标设备" span="2">
           <el-tag
               v-for="device in detailCommand.targetDevices"
               :key="device"
-              size="medium"
+              size="small"
               type="info"
+              class="device-tag"
           >
             {{ device }}
           </el-tag>
         </el-descriptions-item>
         <el-descriptions-item label="指令状态" span="1">
-          <el-badge
-              :text="mapStatus(detailCommand.status)"
+          <el-tag
               :type="getStatusColor(detailCommand.status)"
-              :icon="getStatusIcon(detailCommand.status)"
-          />
+          >
+            {{ mapStatus(detailCommand.status) }}
+          </el-tag>
         </el-descriptions-item>
         <el-descriptions-item label="执行时间" span="1">{{ detailCommand.timestamp }}</el-descriptions-item>
         <el-descriptions-item label="操作员" span="1">{{ detailCommand.operatorId }}</el-descriptions-item>
@@ -133,75 +249,129 @@
       </el-descriptions>
 
       <!-- 响应监控信息 -->
-      <el-card class="mt-4" title="设备响应与执行详情">
+      <el-card class="mt-4">
+        <template #header>
+          <span>设备响应与执行详情</span>
+        </template>
         <el-row :gutter="20">
-          <el-col span="12">
-            <el-card class="response-card">
+          <el-col :span="12">
+            <div class="response-card">
               <div class="card-title">反馈状态</div>
-              <div class="status-icon" :class="getStatusClass(detailCommand.feedbackStatus)"></div>
-              <div>{{ mapFeedbackStatus(detailCommand.feedbackStatus) }}</div>
-            </el-card>
+              <div class="status-icon" :class="getStatusClass(detailCommand.feedbackStatus)">
+                <el-icon v-if="detailCommand.feedbackStatus === 'OK'"><CircleCheck /></el-icon>
+                <el-icon v-else-if="detailCommand.feedbackStatus === 'TIMEOUT'"><Clock /></el-icon>
+                <el-icon v-else><CircleClose /></el-icon>
+              </div>
+              <div class="status-text">{{ mapFeedbackStatus(detailCommand.feedbackStatus) }}</div>
+            </div>
           </el-col>
-          <el-col span="12">
-            <el-card class="execution-card">
+          <el-col :span="12">
+            <div class="execution-card">
               <div class="card-title">执行耗时</div>
               <div class="time-value">{{ detailCommand.executionTime }} ms</div>
-              <div v-if="detailCommand.errorCode !== 'NONE'">
+              <el-progress
+                  :percentage="getTimePercentage(detailCommand.executionTime)"
+                  :color="getProgressColor(detailCommand.executionTime)"
+              />
+              <div v-if="detailCommand.errorCode && detailCommand.errorCode !== 'NONE'" class="error-info">
                 <div class="error-title">异常代码</div>
                 <div class="error-code">{{ detailCommand.errorCode }}</div>
-                <el-tooltip content="点击查看错误详情" placement="top">
-                  <el-button
-                      type="text"
-                      size="small"
-                      @click="showErrorDialog"
-                  >
-                    查看详情
-                  </el-button>
-                </el-tooltip>
+                <el-button
+                    type="primary"
+                    size="small"
+                    link
+                    @click="showErrorDialog"
+                >
+                  查看详情
+                </el-button>
               </div>
-            </el-card>
+            </div>
           </el-col>
         </el-row>
       </el-card>
 
       <!-- 策略配置信息 -->
-      <el-card class="mt-4" title="策略配置与联动规则">
+      <el-card class="mt-4">
+        <template #header>
+          <span>策略配置与联动规则</span>
+        </template>
         <el-row :gutter="16">
-          <el-col span="12">
+          <el-col :span="12">
             <h4>定时任务</h4>
             <p>{{ detailCommand.scheduleTask || "无定时任务" }}</p>
           </el-col>
-          <el-col span="12">
+          <el-col :span="12">
             <h4>场景模式</h4>
             <p>{{ detailCommand.sceneMode || "无场景模式" }}</p>
           </el-col>
         </el-row>
         <el-row :gutter="16" class="mt-4">
-          <el-col span="24">
+          <el-col :span="24">
             <h4>联动规则</h4>
             <p>{{ detailCommand.linkageRule || "无联动配置" }}</p>
           </el-col>
         </el-row>
       </el-card>
 
-      <!-- 错误详情弹窗 -->
-      <el-dialog
-          v-model="errorDialogOpen"
-          title="错误详情"
-          width="400px"
-      >
-        <p>异常代码:{{ detailCommand.errorCode }}</p>
-        <p>处理建议:{{ getErrorSuggestion(detailCommand.errorCode) }}</p>
-      </el-dialog>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="detailDialogOpen = false">关闭</el-button>
+          <el-button type="primary" @click="handlePrintDetail">打印</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 错误详情弹窗 -->
+    <el-dialog
+        v-model="errorDialogOpen"
+        title="错误详情"
+        width="500px"
+    >
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="异常代码">
+          <el-tag type="danger">{{ detailCommand.errorCode }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="错误描述">
+          {{ getErrorDescription(detailCommand.errorCode) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="处理建议">
+          {{ getErrorSuggestion(detailCommand.errorCode) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="相关文档">
+          <el-link type="primary" :underline="false" @click="openErrorDoc(detailCommand.errorCode)">
+            查看解决方案
+          </el-link>
+        </el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="errorDialogOpen = false">关闭</el-button>
+          <el-button type="primary" @click="copyErrorInfo">复制错误信息</el-button>
+        </span>
+      </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue';
-import { ElMessage, ElBadge, ElDialog } from 'element-plus';
+import { ref, reactive, computed, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+  Search,
+  Refresh,
+  Download,
+  DocumentCopy,
+  CircleCheck,
+  CircleClose,
+  Timer,
+  Clock
+} from '@element-plus/icons-vue';
 import {
-  getBuildingEquipmentMonitoringCommandsList
+  getBuildingEquipmentMonitoringCommandsList,
+  retryCommand,
+  cancelCommand,
+  exportCommands,
+  getCommandStats
 } from "@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring";
 
 // 查询参数
@@ -211,25 +381,36 @@ const queryParams = reactive({
   status: '',
   operatorId: '',
   feedbackStatus: '',
+  startTime: '',
+  endTime: '',
   pageNum: 1,
   pageSize: 10,
 });
 
-// 表格数据
+// 数据状态
 const commandList = ref([]);
 const total = ref(0);
 const isLoading = ref(false);
 const detailDialogOpen = ref(false);
 const detailCommand = ref({});
 const errorDialogOpen = ref(false);
+const dateRange = ref([]);
+
+// 统计数据
+const stats = reactive({
+  todayTotal: 0,
+  successCount: 0,
+  failedCount: 0,
+  avgResponseTime: 0
+});
 
 // 指令状态映射
 const mapStatus = (status) => {
   const statusMap = {
     'PENDING': '待执行',
+    'PROCESSING': '执行中',
     'SUCCESS': '成功',
     'FAILED': '失败',
-    'PROCESSING': '处理中',
     'RETRYING': '重试中',
     'CANCELED': '已取消'
   };
@@ -240,59 +421,134 @@ const mapStatus = (status) => {
 const getStatusColor = (status) => {
   const colorMap = {
     'PENDING': 'warning',
+    'PROCESSING': 'primary',
     'SUCCESS': 'success',
     'FAILED': 'danger',
-    'PROCESSING': 'primary',
     'RETRYING': 'info',
     'CANCELED': 'info'
   };
   return colorMap[status] || 'info';
 };
 
+// 反馈状态映射
 const mapFeedbackStatus = (status) => {
-  return {
-    OK: '设备正常响应',
-    TIMEOUT: '响应超时',
-    ERROR: '响应错误',
-  }[status] || status;
+  const feedbackMap = {
+    'OK': '正常响应',
+    'TIMEOUT': '响应超时',
+    'ERROR': '响应错误',
+  };
+  return feedbackMap[status] || status;
+};
+
+// 反馈状态颜色
+const getFeedbackColor = (status) => {
+  const colorMap = {
+    'OK': 'success',
+    'TIMEOUT': 'warning',
+    'ERROR': 'danger'
+  };
+  return colorMap[status] || 'info';
 };
 
 // 指令类型映射
 const mapCmdType = (type) => {
-  return {
-    SET_PARAM: '参数设置',
-    TRIGGER_ALARM: '报警触发',
-    START_STOP: '设备启停',
-    CALIBRATE: '校准操作',
-    RESET: '设备重置',
-  }[type] || type;
+  const typeMap = {
+    'SET_PARAM': '参数设置',
+    'TRIGGER_ALARM': '报警触发',
+    'START_STOP': '设备启停',
+    'CALIBRATE': '校准操作',
+    'RESET': '设备重置',
+  };
+  return typeMap[type] || type;
 };
 
-// 状态图标与颜色
-const getStatusIcon = (status) => {
-  return {
-    PENDING: 'el-icon-clock',
-    SUCCESS: '',
-    FAILED: 'el-icon-error',
-  }[status] || 'el-icon-info';
+// 指令类型颜色
+const getCmdTypeColor = (type) => {
+  const colorMap = {
+    'SET_PARAM': 'primary',
+    'TRIGGER_ALARM': 'danger',
+    'START_STOP': 'success',
+    'CALIBRATE': 'warning',
+    'RESET': 'info'
+  };
+  return colorMap[type] || '';
 };
 
+// 状态样式类
 const getStatusClass = (status) => {
-  return {
-    OK: 'status-ok',
-    TIMEOUT: 'status-timeout',
-    ERROR: 'status-error',
-  }[status] || 'status-unknown';
+  const classMap = {
+    'OK': 'status-ok',
+    'TIMEOUT': 'status-timeout',
+    'ERROR': 'status-error',
+  };
+  return classMap[status] || 'status-unknown';
+};
+
+// 执行时间样式
+const getTimeClass = (time) => {
+  if (time < 100) return 'time-fast';
+  if (time < 500) return 'time-normal';
+  if (time < 1000) return 'time-slow';
+  return 'time-very-slow';
+};
+
+// 时间百分比计算
+const getTimePercentage = (time) => {
+  const maxTime = 2000; // 最大时间2秒
+  return Math.min((time / maxTime) * 100, 100);
+};
+
+// 进度条颜色
+const getProgressColor = (time) => {
+  if (time < 100) return '#67c23a';
+  if (time < 500) return '#409eff';
+  if (time < 1000) return '#e6a23c';
+  return '#f56c6c';
+};
+
+// 错误描述
+const getErrorDescription = (errorCode) => {
+  const descriptions = {
+    'ALARM_MODULE_FAILURE': '报警模块无法正常工作,可能是硬件故障或配置错误',
+    'COMMUNICATION_ERROR': '设备通信中断,无法建立有效连接',
+    'TIMEOUT': '指令执行超时,设备未在规定时间内响应',
+    'PARAMETER_ERROR': '参数设置错误,请检查输入值是否符合要求',
+    'DEVICE_OFFLINE': '目标设备离线,无法执行指令',
+    'PERMISSION_DENIED': '权限不足,当前操作员无权执行此指令',
+    'DEFAULT_ERROR': '未知错误,请联系技术支持',
+  };
+  return descriptions[errorCode] || descriptions['DEFAULT_ERROR'];
 };
 
 // 错误处理建议
 const getErrorSuggestion = (errorCode) => {
-  return {
-    ALARM_MODULE_FAILURE: '检查报警模块连接状态',
-    COMMUNICATION_ERROR: '重启通信设备并检查网络',
-    TIMEOUT: '增加指令超时时间设置',
-    DEFAULT_ERROR: '联系系统管理员',
-  }[errorCode] || 'DEFAULT_ERROR';
+  const suggestions = {
+    'ALARM_MODULE_FAILURE': '1. 检查报警模块连接状态\n2. 重启报警模块\n3. 检查模块配置参数',
+    'COMMUNICATION_ERROR': '1. 检查网络连接\n2. 重启通信设备\n3. 验证通信协议配置',
+    'TIMEOUT': '1. 增加指令超时时间设置\n2. 检查设备响应性能\n3. 优化网络延迟',
+    'PARAMETER_ERROR': '1. 核对参数格式要求\n2. 检查参数取值范围\n3. 参考设备手册',
+    'DEVICE_OFFLINE': '1. 检查设备电源\n2. 验证设备网络连接\n3. 查看设备状态日志',
+    'PERMISSION_DENIED': '1. 联系管理员授权\n2. 使用具有相应权限的账号\n3. 查看权限配置',
+    'DEFAULT_ERROR': '请联系系统管理员或技术支持团队',
+  };
+  return suggestions[errorCode] || suggestions['DEFAULT_ERROR'];
+};
+
+// 表格行样式
+const tableRowClassName = ({ row }) => {
+  if (row.status === 'FAILED') return 'error-row';
+  if (row.status === 'PROCESSING') return 'processing-row';
+  return '';
+};
+
+// 获取统计数据
+const getStats = async () => {
+  try {
+    const response = await getCommandStats();
+    Object.assign(stats, response.data);
+  } catch (error) {
+    console.error('获取统计数据失败:', error);
+  }
 };
 
 // 获取数据
@@ -300,8 +556,8 @@ const getList = async () => {
   isLoading.value = true;
   try {
     const response = await getBuildingEquipmentMonitoringCommandsList(queryParams);
-    commandList.value = response.data.list;
-    total.value = response.data.total;
+    commandList.value = response.data.list || [];
+    total.value = response.data.total || 0;
   } catch (error) {
     ElMessage.error('获取指令数据失败');
     console.error(error);
@@ -314,17 +570,36 @@ const getList = async () => {
 const handleSearch = () => {
   queryParams.pageNum = 1;
   getList();
+  getStats();
 };
 
 // 重置
 const resetSearch = () => {
-  queryParams.cmdId = '';
-  queryParams.cmdType = '';
-  queryParams.status = '';
-  queryParams.operatorId = '';
-  queryParams.feedbackStatus = '';
-  queryParams.pageNum = 1;
+  Object.assign(queryParams, {
+    cmdId: '',
+    cmdType: '',
+    status: '',
+    operatorId: '',
+    feedbackStatus: '',
+    startTime: '',
+    endTime: '',
+    pageNum: 1,
+    pageSize: 10,
+  });
+  dateRange.value = [];
   getList();
+  getStats();
+};
+
+// 日期范围变化
+const handleDateChange = (value) => {
+  if (value) {
+    queryParams.startTime = value[0];
+    queryParams.endTime = value[1];
+  } else {
+    queryParams.startTime = '';
+    queryParams.endTime = '';
+  }
 };
 
 // 分页
@@ -339,74 +614,353 @@ const handleSizeChange = (newSize) => {
   getList();
 };
 
-// 行点击事件
-const handleRowClick = (row) => {
+// 查看详情
+const handleViewDetail = (row) => {
   detailCommand.value = { ...row };
   detailDialogOpen.value = true;
 };
 
+// 行点击事件
+const handleRowClick = (row) => {
+  handleViewDetail(row);
+};
+
+// 重试指令
+const handleRetry = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+        `确定要重试指令 ${row.cmdId} 吗?`,
+        '提示',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }
+    );
+
+    await retryCommand(row.cmdId);
+    ElMessage.success('重试指令已发送');
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('重试失败');
+    }
+  }
+};
+
+// 取消指令
+const handleCancel = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+        `确定要取消指令 ${row.cmdId} 吗?`,
+        '提示',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }
+    );
+
+    await cancelCommand(row.cmdId);
+    ElMessage.success('指令已取消');
+    getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('取消失败');
+    }
+  }
+};
+
+// 导出数据
+const handleExport = async () => {
+  try {
+    isLoading.value = true;
+    await exportCommands(queryParams);
+    ElMessage.success('导出成功');
+  } catch (error) {
+    ElMessage.error('导出失败');
+  } finally {
+    isLoading.value = false;
+  }
+};
+
+// 打印详情
+const handlePrintDetail = () => {
+  window.print();
+};
+
 // 显示错误详情
 const showErrorDialog = () => {
   errorDialogOpen.value = true;
 };
 
-getList();
+// 复制错误信息
+const copyErrorInfo = () => {
+  const errorInfo = `错误代码: ${detailCommand.value.errorCode}\n错误描述: ${getErrorDescription(detailCommand.value.errorCode)}\n处理建议: ${getErrorSuggestion(detailCommand.value.errorCode)}`;
+  navigator.clipboard.writeText(errorInfo).then(() => {
+    ElMessage.success('错误信息已复制到剪贴板');
+  });
+};
+
+// 打开错误文档
+const openErrorDoc = (errorCode) => {
+  window.open(`/docs/error/${errorCode}`, '_blank');
+};
+
+// 初始化
+onMounted(() => {
+  getList();
+  getStats();
+});
 </script>
 
 <style scoped>
+.control-command-system {
+  padding: 20px;
+}
+
 .filter-card {
   margin-bottom: 20px;
-  padding: 20px;
+}
+
+.search-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.stats-row {
+  margin-bottom: 20px;
+}
+
+.stats-card {
+  text-align: center;
+  transition: all 0.3s;
+}
+
+.stats-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.success-card :deep(.el-statistic__number) {
+  color: #67c23a;
+}
+
+.error-card :deep(.el-statistic__number) {
+  color: #f56c6c;
 }
 
 .data-table {
   min-height: 500px;
 }
 
-.el-badge {
-  font-size: 14px;
+.device-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 5px;
+  justify-content: center;
+}
+
+.device-tag {
+  margin: 2px;
+}
+
+.processing-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  background-color: #409eff;
+  border-radius: 50%;
+  margin-right: 5px;
+  animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+  0% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.5;
+  }
+  100% {
+    opacity: 1;
+  }
+}
+
+.time-fast {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.time-normal {
+  color: #409eff;
+}
+
+.time-slow {
+  color: #e6a23c;
+}
+
+.time-very-slow {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+:deep(.error-row) {
+  background-color: #fef0f0;
+}
+
+:deep(.processing-row) {
+  background-color: #ecf5ff;
 }
 
 .response-card, .execution-card {
-  height: 150px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
+  padding: 20px;
+  text-align: center;
+  background: #f5f7fa;
   border-radius: 8px;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  min-height: 200px;
 }
 
 .status-icon {
-  width: 48px;
-  height: 48px;
+  width: 60px;
+  height: 60px;
   border-radius: 50%;
-  margin-bottom: 12px;
+  margin: 20px auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30px;
+  color: white;
+}
+
+.status-ok {
+  background-color: #67c23a;
 }
 
-.status-ok { background-color: #67c23a; }
-.status-timeout { background-color: #e6a23c; }
-.status-error { background-color: #f56c6c; }
+.status-timeout {
+  background-color: #e6a23c;
+}
 
-.time-value {
-  font-size: 24px;
+.status-error {
+  background-color: #f56c6c;
+}
+
+.status-unknown {
+  background-color: #909399;
+}
+
+.status-text {
+  font-size: 16px;
   font-weight: 500;
-  color: #333;
+  margin-top: 10px;
+}
+
+.time-value {
+  font-size: 36px;
+  font-weight: bold;
+  color: #303133;
+  margin: 20px 0;
+}
+
+.error-info {
+  margin-top: 20px;
+  padding: 15px;
+  background: #fef0f0;
+  border-radius: 4px;
+  border: 1px solid #fde2e2;
 }
 
 .error-title {
   color: #f56c6c;
-  margin-top: 16px;
+  font-weight: 500;
+  margin-bottom: 5px;
 }
 
 .error-code {
   font-size: 14px;
   color: #666;
+  margin-bottom: 10px;
 }
 
 .card-title {
-  font-size: 16px;
+  font-size: 18px;
   font-weight: 500;
-  margin-bottom: 8px;
+  color: #303133;
+  margin-bottom: 15px;
+}
+
+.mt-4 {
+  margin-top: 20px;
+}
+
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+/* 打印样式 */
+@media print {
+  .filter-card,
+  .stats-row,
+  .el-table__column-filter-trigger,
+  .pagination,
+  .dialog-footer {
+    display: none !important;
+  }
+
+  .data-table {
+    box-shadow: none !important;
+  }
+}
+
+/* 响应式布局 */
+@media (max-width: 768px) {
+  .search-form {
+    display: block;
+  }
+
+  .search-form .el-form-item {
+    margin-bottom: 10px;
+  }
+
+  .stats-row .el-col {
+    margin-bottom: 10px;
+  }
+
+  .el-dialog {
+    width: 95% !important;
+  }
+}
+
+/* 加载动画 */
+.el-loading-mask {
+  background-color: rgba(255, 255, 255, 0.9);
+}
+
+/* 自定义滚动条 */
+.el-table__body-wrapper::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.el-table__body-wrapper::-webkit-scrollbar-track {
+  background: #f1f1f1;
+}
+
+.el-table__body-wrapper::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 4px;
+}
+
+.el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
+  background: #909399;
 }
 </style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 776 - 202
pm_ui/src/views/repairOrder/repairOrder/index.vue


+ 955 - 225
pm_ui/src/views/repairOrder/repairOrder/index2.vue

@@ -1,220 +1,458 @@
 <template>
-  <div class="app-container">
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="工单编号" prop="orderNo">
-        <el-input
-            v-model="queryParams.orderNo"
-            placeholder="请输入工单编号"
-            clearable
-            @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="项目名称" prop="projectName">
-        <el-input
-            v-model="queryParams.projectName"
-            placeholder="请输入项目名称"
-            clearable
-            @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="工单状态" prop="orderStatus" style="width: 260px">
-        <el-select v-model="queryParams.orderStatus" placeholder="请选择工单状态" clearable >
-          <el-option
-              v-for="dict in repair_status"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-<!--    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-            type="primary"
-            plain
-            icon="Plus"
-            @click="handleAdd"
-            v-hasPermi="['repairOrder:repairOrder:add']"
-        >新增</el-button>
+  <div class="repair-order-container">
+    <!-- 页面标题 -->
+    <div class="page-header">
+      <h2 class="page-title">
+        <el-icon><Tools /></el-icon>
+        维修工单管理
+      </h2>
+      <div class="header-actions">
+<!--        <el-button type="primary" :icon="Plus" @click="handleAdd" v-hasPermi="['repairOrder:repairOrder:add']">
+          新建工单
+        </el-button>-->
+        <el-button :icon="Download" @click="handleExport" v-hasPermi="['repairOrder:repairOrder:export']">
+          导出报表
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 统计卡片 -->
+    <el-row :gutter="20" class="statistics-cards">
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon blue">
+            <el-icon><DocumentCopy /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.total }}</div>
+            <div class="stat-label">工单总数</div>
+          </div>
+        </div>
       </el-col>
-      <el-col :span="1.5">
-        <el-button
-            type="success"
-            plain
-            icon="Edit"
-            :disabled="single"
-            @click="handleUpdate"
-            v-hasPermi="['repairOrder:repairOrder:edit']"
-        >修改</el-button>
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon orange">
+            <el-icon><Clock /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.pending }}</div>
+            <div class="stat-label">待处理</div>
+          </div>
+        </div>
       </el-col>
-      <el-col :span="1.5">
-        <el-button
-            type="danger"
-            plain
-            icon="Delete"
-            :disabled="multiple"
-            @click="handleDelete"
-            v-hasPermi="['repairOrder:repairOrder:remove']"
-        >删除</el-button>
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon purple">
+            <el-icon><Loading /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.processing }}</div>
+            <div class="stat-label">处理中</div>
+          </div>
+        </div>
       </el-col>
-      <el-col :span="1.5">
-        <el-button
-            type="warning"
-            plain
-            icon="Download"
-            @click="handleExport"
-            v-hasPermi="['repairOrder:repairOrder:export']"
-        >导出</el-button>
+      <el-col :xs="24" :sm="12" :md="6">
+        <div class="stat-card">
+          <div class="stat-icon green">
+            <el-icon><CircleCheck /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.completed }}</div>
+            <div class="stat-label">已完成</div>
+          </div>
+        </div>
       </el-col>
-      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>-->
-
-    <el-table v-loading="loading" :data="repairOrderList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-<!--      <el-table-column label="id" align="center" prop="id" />-->
-      <el-table-column label="工单编号" align="center" prop="orderNo" />
-      <el-table-column label="工单内容" align="center" prop="orderContent" />
-      <el-table-column label="项目名称" align="center" prop="projectName" />
-      <el-table-column label="派单时间" align="center" prop="assignTime" width="180" sortable>
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.assignTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="责任人" align="center" prop="finishBy" />
-      <el-table-column label="工单状态" align="center" prop="orderStatus">
-        <template #default="scope">
-          <dict-tag :options="repair_status" :value="scope.row.orderStatus"/>
-        </template>
-      </el-table-column>
-      <el-table-column label="完成时间" align="center" prop="finishTime" width="180">
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="附件" align="center" prop="annex" />
-      <!--      <el-table-column label="完成人id" align="center" prop="userId" />-->
-      <el-table-column label="工单备注" align="center" prop="orderRemark" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200px">
-        <template #default="scope">
-<!--          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['repairOrder:repairOrder:edit']">详情</el-button>-->
-          <el-button link type="primary" icon="View" @click="showDetail(scope.row)" v-hasPermi="['repairOrder:repairOrder:edit']">详情</el-button>
-          <el-button
-              v-if="scope.row.orderStatus === '1'"
-              link
-              type="primary"
-              icon="Edit"
-              @click="handleUpdate(scope.row)"
-              v-hasPermi="['repairOrder:repairOrder:edit']"
-          >处理</el-button>
-<!--          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['repairOrder:repairOrder:remove']">删除</el-button>-->
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination
-        v-show="total>0"
-        :total="total"
-        v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-    />
+    </el-row>
+
+    <!-- 搜索栏 -->
+    <el-card class="search-card" shadow="never">
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+        <el-row :gutter="20">
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="工单编号" prop="orderNo">
+              <el-input
+                  v-model="queryParams.orderNo"
+                  placeholder="请输入工单编号"
+                  clearable
+                  :prefix-icon="Search"
+                  @keyup.enter="handleQuery"
+                  style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="项目名称" prop="projectName">
+              <el-input
+                  v-model="queryParams.projectName"
+                  placeholder="请输入项目名称"
+                  clearable
+                  :prefix-icon="Search"
+                  @keyup.enter="handleQuery"
+                  style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="工单状态" prop="orderStatus">
+              <el-select
+                  v-model="queryParams.orderStatus"
+                  placeholder="请选择工单状态"
+                  clearable
+                  style="width: 100%;min-width: 200px;"
+              >
+                <el-option
+                    v-for="dict in repair_status"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value"
+                >
+                  <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
+                    <span>{{ dict.label }}</span>
+                    <el-tag :type="getStatusTagType(dict.value)" size="small">
+                      {{ dict.label }}
+                    </el-tag>
+                  </div>
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6">
+            <el-form-item label="派单时间" prop="dateRange">
+              <el-date-picker
+                  v-model="queryParams.dateRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  format="YYYY-MM-DD"
+                  value-format="YYYY-MM-DD"
+                  style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24" style="text-align: right;">
+            <el-form-item>
+              <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+
+    <!-- 数据表格 -->
+    <el-card class="table-card" shadow="never">
+      <div class="table-header">
+        <span class="table-title">工单列表</span>
+        <div class="table-tools">
+          <el-tooltip content="刷新" placement="top">
+            <el-button :icon="Refresh" circle size="small" @click="getList" />
+          </el-tooltip>
+          <el-tooltip content="显示/隐藏搜索" placement="top">
+            <el-button
+                :icon="showSearch ? View : Hide"
+                circle
+                size="small"
+                @click="showSearch = !showSearch"
+            />
+          </el-tooltip>
+        </div>
+      </div>
 
+      <el-table
+          v-loading="loading"
+          :data="repairOrderList"
+          @selection-change="handleSelectionChange"
+          stripe
+          border
+          :row-class-name="tableRowClassName"
+      >
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="序号" type="index" width="60" align="center">
+          <template #default="scope">
+            {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="工单编号" align="center" prop="orderNo" min-width="120">
+          <template #default="scope">
+            <el-link type="primary" @click="showDetail(scope.row)">
+              {{ scope.row.orderNo }}
+            </el-link>
+          </template>
+        </el-table-column>
+        <el-table-column label="工单内容" align="center" prop="orderContent" min-width="200" show-overflow-tooltip />
+        <el-table-column label="项目名称" align="center" prop="projectName" min-width="150" show-overflow-tooltip />
+        <el-table-column label="派单时间" align="center" prop="assignTime" width="180" sortable>
+          <template #default="scope">
+            <div class="time-info">
+              <el-icon><Clock /></el-icon>
+              <span>{{ parseTime(scope.row.assignTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="责任人" align="center" prop="finishBy" min-width="100">
+          <template #default="scope">
+            <div class="user-info">
+              <el-avatar size="small" :src="getUserAvatar(scope.row.finishBy)">
+                {{ scope.row.finishBy?.charAt(0) || '无' }}
+              </el-avatar>
+              <span>{{ scope.row.finishBy || '未分配' }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="工单状态" align="center" prop="orderStatus" width="100">
+          <template #default="scope">
+            <el-tag
+                :type="getStatusTagType(scope.row.orderStatus)"
+                effect="dark"
+                size="small"
+            >
+              <el-icon v-if="scope.row.orderStatus === '0'"><Clock /></el-icon>
+              <el-icon v-else-if="scope.row.orderStatus === '1'"><Loading /></el-icon>
+              <el-icon v-else-if="scope.row.orderStatus === '2'"><CircleCheck /></el-icon>
+              {{ getStatusLabel(scope.row.orderStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="完成时间" align="center" prop="finishTime" width="180">
+          <template #default="scope">
+            <span v-if="scope.row.finishTime">
+              {{ parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}') }}
+            </span>
+            <span v-else class="text-muted">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="附件" align="center" prop="annex" width="80">
+          <template #default="scope">
+            <el-button
+                v-if="scope.row.annex"
+                type="primary"
+                link
+                :icon="Paperclip"
+                @click="downloadFile(scope.row.annex)"
+            >
+              查看
+            </el-button>
+            <span v-else class="text-muted">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
+          <template #default="scope">
+            <el-button
+                link
+                type="primary"
+                :icon="View"
+                @click="showDetail(scope.row)"
+                v-hasPermi="['repairOrder:repairOrder:edit']"
+            >
+              详情
+            </el-button>
+            <el-button
+                v-if="scope.row.orderStatus === '0' || scope.row.orderStatus === '1'"
+                link
+                type="warning"
+                :icon="Edit"
+                @click="handleUpdate(scope.row)"
+                v-hasPermi="['repairOrder:repairOrder:edit']"
+            >
+              处理
+            </el-button>
+            <el-button
+                link
+                type="danger"
+                :icon="Delete"
+                @click="handleDelete(scope.row)"
+                v-hasPermi="['repairOrder:repairOrder:remove']"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+          v-show="total > 0"
+          :total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 工单详情抽屉 -->
     <el-drawer
         v-model="detailVisible"
         title="工单详情"
         direction="rtl"
-        size="40%">
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="工单编号">{{ detailData.orderNo }}</el-descriptions-item>
-        <el-descriptions-item label="工单内容">{{ detailData.orderContent }}</el-descriptions-item>
-        <el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
-        <el-descriptions-item label="派单时间">{{ parseTime(detailData.assignTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
-        <el-descriptions-item label="完成时间">{{ parseTime(detailData.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
-        <el-descriptions-item label="责任人">{{ detailData.finishBy }}</el-descriptions-item>
-        <el-descriptions-item label="工单状态">
-          <dict-tag :options="repair_status" :value="detailData.orderStatus"/>
-        </el-descriptions-item>
-        <el-descriptions-item label="工单备注">{{ detailData.orderRemark }}</el-descriptions-item>
-        <el-descriptions-item label="附件">{{ detailData.annex }}</el-descriptions-item>
-
-      </el-descriptions>
-      <el-button type="primary" @click="downloadFile(detailData.annex)">下载附件</el-button>
+        size="50%"
+        class="detail-drawer"
+    >
+      <template #header>
+        <div class="drawer-header">
+          <h3>工单详情</h3>
+          <el-tag :type="getStatusTagType(detailData.orderStatus)" size="large">
+            {{ getStatusLabel(detailData.orderStatus) }}
+          </el-tag>
+        </div>
+      </template>
+
+      <div class="detail-content">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="工单编号" :span="1">
+            <span class="detail-value">{{ detailData.orderNo }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="项目名称" :span="1">
+            {{ detailData.projectName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="工单内容" :span="2">
+            {{ detailData.orderContent }}
+          </el-descriptions-item>
+          <el-descriptions-item label="派单时间">
+            {{ parseTime(detailData.assignTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+          </el-descriptions-item>
+          <el-descriptions-item label="完成时间">
+            {{ detailData.finishTime ? parseTime(detailData.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') : '未完成' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="责任人">
+            <div class="user-info">
+              <el-avatar size="small" :src="getUserAvatar(detailData.finishBy)">
+                {{ detailData.finishBy?.charAt(0) || '无' }}
+              </el-avatar>
+              <span>{{ detailData.finishBy || '未分配' }}</span>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="工单状态">
+            <el-tag :type="getStatusTagType(detailData.orderStatus)">
+              {{ getStatusLabel(detailData.orderStatus) }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="工单备注" :span="2">
+            {{ detailData.orderRemark || '无' }}
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <div v-if="detailData.annex" class="attachment-section">
+          <h4>附件信息</h4>
+          <el-button type="primary" :icon="Download" @click="downloadFile(detailData.annex)">
+            下载附件
+          </el-button>
+        </div>
+      </div>
     </el-drawer>
 
     <!-- 添加或修改维修工单对话框 -->
-    <el-dialog :title="title" v-model="open" width="600px" append-to-body>
-      <el-form ref="repairOrderRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="工单编号" prop="orderNo">
-          <el-input v-model="form.orderNo" placeholder="请输入工单编号" disabled/>
-        </el-form-item>
+    <el-dialog
+        :title="title"
+        v-model="open"
+        width="700px"
+        append-to-body
+        :close-on-click-modal="false"
+    >
+      <el-form ref="repairOrderRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="工单编号" prop="orderNo">
+              <el-input v-model="form.orderNo" placeholder="请输入工单编号" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="项目名称" prop="projectName">
+              <el-input v-model="form.projectName" placeholder="请输入项目名称" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
         <el-form-item label="工单内容" prop="orderContent">
-          <el-input v-model="form.orderContent" type="textarea" placeholder="请输入内容" disabled/>
-        </el-form-item>
-        <el-form-item label="项目名称" prop="projectName">
-          <el-input v-model="form.projectName" placeholder="请输入项目名称" disabled/>
-        </el-form-item>
-        <el-form-item label="派单时间" prop="assignTime" >
-          <el-date-picker clearable
-                          v-model="form.assignTime"
-                          type="date"
-                          value-format="YYYY-MM-DD"
-                          placeholder="请选择派单时间" disabled>
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="完成时间" prop="finishTime" :rules="rules.finishTime">
-          <el-date-picker clearable
-                          v-model="form.finishTime"
-                          type="datetime"
-                          value-format="YYYY-MM-DD HH:mm:ss"
-                          placeholder="请选择完成时间">
-          </el-date-picker>
+          <el-input
+              v-model="form.orderContent"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入内容"
+              disabled
+          />
         </el-form-item>
-        <el-form-item label="责任人" prop="finishBy" >
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="派单时间" prop="assignTime">
+              <el-date-picker
+                  v-model="form.assignTime"
+                  type="datetime"
+                  value-format="YYYY-MM-DD HH:mm:ss"
+                  placeholder="请选择派单时间"
+                  disabled
+                  style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="完成时间" prop="finishTime" :rules="rules.finishTime">
+              <el-date-picker
+                  v-model="form.finishTime"
+                  type="datetime"
+                  value-format="YYYY-MM-DD HH:mm:ss"
+                  placeholder="请选择完成时间"
+                  style="width: 100%"
+                  :disabled-date="disabledDate"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="责任人" prop="finishBy">
           <el-select
               v-model="form.userId"
               placeholder="请选择责任人"
               clearable
               filterable
               disabled
+              style="width: 100%"
           >
             <el-option
                 v-for="user in userOptions"
                 :key="user.userId"
-                :label="user.userName || user.userName"
+                :label="user.nickName || user.userName"
                 :value="user.userId"
-            />
+            >
+              <div style="display: flex; align-items: center;">
+                <el-avatar size="small" style="margin-right: 8px">
+                  {{ (user.nickName || user.userName).charAt(0) }}
+                </el-avatar>
+                <span>{{ user.nickName || user.userName }}</span>
+              </div>
+            </el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="附件" prop="annex">
-          <file-upload v-model="form.annex" :on-remove="handleRemoveFile"/>
+
+        <el-form-item label="附件上传" prop="annex">
+          <file-upload
+              v-model="form.annex"
+              :on-remove="handleRemoveFile"
+              :limit="5"
+              :file-size="10"
+          />
         </el-form-item>
-        <!--
-        <el-form-item label="完成人id" prop="userId">
-          <el-input v-model="form.userId" placeholder="请输入完成人id" />
-        </el-form-item>-->
-<!--        <el-form-item label="工单状态" prop="orderStatus">
-          <el-radio-group v-model="form.orderStatus">
-            <el-radio
-                v-for="dict in repair_status"
-                :key="dict.value"
-                :label="dict.value"
-            >{{dict.label}}</el-radio>
-          </el-radio-group>
-        </el-form-item>-->
+
         <el-form-item label="工单备注" prop="orderRemark">
-          <el-input v-model="form.orderRemark" type="textarea" placeholder="请输入内容" />
+          <el-input
+              v-model="form.orderRemark"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入工单处理备注"
+              maxlength="500"
+              show-word-limit
+          />
         </el-form-item>
       </el-form>
+
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
+          <el-button type="primary" @click="submitForm" :loading="submitLoading">
+            确 定
+          </el-button>
         </div>
       </template>
     </el-dialog>
@@ -222,15 +460,43 @@
 </template>
 
 <script setup name="RepairOrder">
-import { listRepairOrder, getRepairOrder, delRepairOrder, addRepairOrder, updateRepairOrder,deleteFile } from "@/api/repairOrder/repairOrder";
+import { ref, reactive, toRefs, onMounted, computed } from 'vue';
+import {
+  listRepairOrder,
+  getRepairOrder,
+  delRepairOrder,
+  addRepairOrder,
+  updateRepairOrder,
+  deleteFile, getRepairOrderStatistics
+} from "@/api/repairOrder/repairOrder";
 import { listUser } from "@/api/system/user";
+import { ElMessage, ElMessageBox } from 'element-plus';
+// 导入图标
+import {
+  Tools,
+  Plus,
+  Download,
+  DocumentCopy,
+  Clock,
+  Loading,
+  CircleCheck,
+  Search,
+  Refresh,
+  View,
+  Hide,
+  Edit,
+  Delete,
+  Paperclip
+} from '@element-plus/icons-vue';
 
 const { proxy } = getCurrentInstance();
 const { repair_status } = proxy.useDict('repair_status');
 
+// 响应式变量
 const repairOrderList = ref([]);
 const open = ref(false);
 const loading = ref(true);
+const submitLoading = ref(false);
 const showSearch = ref(true);
 const ids = ref([]);
 const single = ref(true);
@@ -238,9 +504,17 @@ const multiple = ref(true);
 const total = ref(0);
 const title = ref("");
 const userOptions = ref([]);
-
 const detailVisible = ref(false);
 const detailData = ref({});
+const statisticsData = ref({
+  total: 0,
+  pending: 0,
+  processing: 0,
+  completed: 0
+});
+
+// 统计数据
+const statistics = computed(() => statisticsData.value);
 
 const data = reactive({
   form: {},
@@ -252,46 +526,141 @@ const data = reactive({
     orderContent: null,
     projectName: null,
     orderStatus: null,
-    state:Date.now(),
+    dateRange: [],
+    state: Date.now(),
   },
   rules: {
     finishTime: [
       { required: true, message: "完成时间不能为空", trigger: "blur" }
+    ],
+    orderRemark: [
+      { required: true, message: "请填写处理备注", trigger: "blur" }
     ]
   }
 });
 
 const { queryParams, form, rules } = toRefs(data);
 
+// 获取状态标签类型
+function getStatusTagType(status) {
+  const statusMap = {
+    '0': 'warning',
+    '1': 'primary',
+    '2': 'success'
+  };
+  return statusMap[status] || 'info';
+}
+
+// 获取状态标签文本
+function getStatusLabel(status) {
+  const dict = repair_status.value.find(item => item.value === status);
+  return dict ? dict.label : '未知';
+}
+
+// 获取用户头像
+function getUserAvatar(userName) {
+  // 这里可以根据实际情况返回用户头像URL
+  return '';
+}
+
+// 表格行样式
+function tableRowClassName({ row }) {
+  if (row.orderStatus === '0') {
+    return 'warning-row';
+  } else if (row.orderStatus === '2') {
+    return 'success-row';
+  }
+  return '';
+}
+
+// 禁用日期(不能选择派单时间之前的日期)
+function disabledDate(time) {
+  if (form.value.assignTime) {
+    return time.getTime() < new Date(form.value.assignTime).getTime();
+  }
+  return false;
+}
+
+// 显示详情
 function showDetail(row) {
-  detailData.value = {...row};
+  detailData.value = { ...row };
   detailVisible.value = true;
 }
 
+// 下载文件
 function downloadFile(url) {
-  window.open(import.meta.env.VITE_APP_BASE_API+url);
+  if (!url) {
+    ElMessage.warning('暂无附件');
+    return;
+  }
+  window.open(import.meta.env.VITE_APP_BASE_API + url);
 }
 
+
 /** 查询维修工单列表 */
-function getList() {
+async function getList() {
   loading.value = true;
-  listRepairOrder(queryParams.value).then(response => {
+  try {
+    const params = { ...queryParams.value };
+
+    // 处理日期范围
+    /*if (dateRange.value && dateRange.value.length === 2) {
+      params.startDate = dateRange.value[0];
+      params.endDate = dateRange.value[1];
+    }*/
+
+    const response = await listRepairOrder(params);
     repairOrderList.value = response.rows;
     total.value = response.total;
+
+    // 获取统计数据
+    await getStatistics();
+  } catch (error) {
+    console.error('获取数据失败:', error);
+    ElMessage.error('获取工单列表失败');
+  } finally {
     loading.value = false;
-  });
+  }
+}
+
+async function getStatistics() {
+  try {
+    // 如果后端没有统计接口,可以通过查询所有数据来统计
+    const response = await getRepairOrderStatistics({isMe: true});
+
+    // 直接更新 statisticsData
+    statisticsData.value = {
+      total: response.data.total || 0,
+      pending: response.data.pending || 0,
+      processing: response.data.processing || 0,
+      completed: response.data.completed || 0
+    };
+
+    // 计算趋势(这里模拟数据,实际应该对比上期数据)
+    statisticsData.value.forEach(stat => {
+      stat.trend = Math.floor(Math.random() * 30) - 15; // 随机生成-15到15的趋势
+    });
+  } catch (error) {
+    console.error('获取统计数据失败:', error);
+  }
 }
+
+// 删除附件
 function handleRemoveFile(file) {
-  // 这里可以添加调用后端API删除文件的逻辑
-  proxy.$modal.confirm('确认删除该附件吗?').then(() => {
+  ElMessageBox.confirm('确认删除该附件吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
     deleteFile(file.url).then(() => {
       form.value.annex = null;
-      proxy.$modal.msgSuccess("删除成功");
+      ElMessage.success("删除成功");
     }).catch(() => {
-      proxy.$modal.msgError("删除失败");
+      ElMessage.error("删除失败");
     });
   }).catch(() => {});
 }
+
 // 取消按钮
 function cancel() {
   open.value = false;
@@ -329,13 +698,14 @@ function handleQuery() {
 /** 重置按钮操作 */
 function resetQuery() {
   proxy.resetForm("queryRef");
+  queryParams.value.dateRange = [];
   handleQuery();
 }
 
 // 多选框选中数据
 function handleSelectionChange(selection) {
   ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
+  single.value = selection.length !== 1;
   multiple.value = !selection.length;
 }
 
@@ -343,8 +713,9 @@ function handleSelectionChange(selection) {
 function handleAdd() {
   reset();
   open.value = true;
-  title.value = "添加维修工单";
+  title.value = "新建维修工单";
 }
+
 // 获取用户列表
 function getUserList() {
   listUser().then(response => {
@@ -352,44 +723,49 @@ function getUserList() {
   });
 }
 
-// 在组件挂载时获取用户列表
-onMounted(() => {
-  getUserList();
-});
 /** 修改按钮操作 */
 function handleUpdate(row) {
   reset();
-  const _id = row.id || ids.value
+  const _id = row.id || ids.value;
   getRepairOrder(_id).then(response => {
     form.value = response.data;
     open.value = true;
-    title.value = "维修工单派单";
+    title.value = "处理维修工单";
   });
-
 }
 
 /** 提交按钮 */
 function submitForm() {
-  if (form.value.userId) {
-    const user = userOptions.value.find(u => u.userId === form.value.userId);
-    if (user) {
-      form.value.finishBy = user.nickName || user.userName;
-      form.value.userId = user.userId || user.userId;
-    }
-  }
   proxy.$refs["repairOrderRef"].validate(valid => {
     if (valid) {
+      submitLoading.value = true;
+
+      // 设置完成人信息
+      if (form.value.userId) {
+        const user = userOptions.value.find(u => u.userId === form.value.userId);
+        if (user) {
+          form.value.finishBy = user.nickName || user.userName;
+        }
+      }
+
+      // 设置工单状态为已完成
+      form.value.orderStatus = '2';
+
       if (form.value.id != null) {
         updateRepairOrder(form.value).then(response => {
-          proxy.$modal.msgSuccess("修改成功");
+          ElMessage.success("处理成功");
           open.value = false;
           getList();
+        }).finally(() => {
+          submitLoading.value = false;
         });
       } else {
         addRepairOrder(form.value).then(response => {
-          proxy.$modal.msgSuccess("新增成功");
+          ElMessage.success("新增成功");
           open.value = false;
           getList();
+        }).finally(() => {
+          submitLoading.value = false;
         });
       }
     }
@@ -400,7 +776,12 @@ function submitForm() {
 function handleDelete(row) {
   const _ids = row.id || ids.value;
   const deletePromises = [];
-  proxy.$modal.confirm('是否确认删除选中的数据项?').then(function() {
+
+  ElMessageBox.confirm('是否确认删除选中的工单?删除后不可恢复!', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
     // 如果是批量删除,检查每个选中项是否有附件
     if (Array.isArray(ids.value) && ids.value.length > 0) {
       repairOrderList.value.forEach(item => {
@@ -413,26 +794,375 @@ function handleDelete(row) {
     else if (row.annex) {
       deletePromises.push(deleteFile(row.annex));
     }
+
     // 先删除所有附件
-    return Promise.all(deletePromises)
-        .then(() => {
-          // 附件删除成功后删除数据
-          return delRepairOrder(_ids);
-        });
+    return Promise.all(deletePromises).then(() => {
+      // 附件删除成功后删除数据
+      return delRepairOrder(_ids);
+    });
   }).then(() => {
     getList();
-    proxy.$modal.msgSuccess("删除成功");
-  }).catch(() => {
-    proxy.$modal.msgError("删除失败");
+    ElMessage.success("删除成功");
+  }).catch((error) => {
+    if (error !== 'cancel') {
+      ElMessage.error("删除失败");
+    }
   });
 }
 
 /** 导出按钮操作 */
 function handleExport() {
-  proxy.download('repairOrder/repairOrder/export', {
-    ...queryParams.value
-  }, `repairOrder_${new Date().getTime()}.xlsx`)
+  ElMessageBox.confirm('确定要导出所有维修工单数据吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'info'
+  }).then(() => {
+    proxy.download('repairOrder/repairOrder/export', {
+      ...queryParams.value
+    }, `维修工单_${new Date().getTime()}.xlsx`);
+  });
 }
 
-getList();
+// 初始化
+onMounted(() => {
+  getList();
+  getUserList();
+});
 </script>
+
+<style scoped lang="scss">
+.repair-order-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: calc(100vh - 84px);
+
+  .page-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .page-title {
+      font-size: 24px;
+      font-weight: 600;
+      color: #303133;
+      margin: 0;
+      display: flex;
+      align-items: center;
+
+      .el-icon {
+        margin-right: 8px;
+        color: #409eff;
+      }
+    }
+
+    .header-actions {
+      display: flex;
+      gap: 10px;
+    }
+  }
+
+  // 统计卡片
+  .statistics-cards {
+    margin-bottom: 20px;
+
+    .stat-card {
+      background: #fff;
+      border-radius: 8px;
+      padding: 20px;
+      display: flex;
+      align-items: center;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+      transition: all 0.3s;
+      margin-bottom: 10px;
+
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.08);
+      }
+
+      .stat-icon {
+        width: 60px;
+        height: 60px;
+        border-radius: 12px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-right: 16px;
+
+        .el-icon {
+          font-size: 28px;
+        }
+
+        &.blue {
+          background: #e6f2ff;
+          color: #409eff;
+        }
+
+        &.orange {
+          background: #fdf6ec;
+          color: #e6a23c;
+        }
+
+        &.purple {
+          background: #f3e8ff;
+          color: #9c27b0;
+        }
+
+        &.green {
+          background: #e8f5e9;
+          color: #67c23a;
+        }
+      }
+
+      .stat-content {
+        flex: 1;
+
+        .stat-value {
+          font-size: 28px;
+          font-weight: 600;
+          color: #303133;
+          line-height: 1;
+          margin-bottom: 8px;
+        }
+
+        .stat-label {
+          font-size: 14px;
+          color: #909399;
+        }
+      }
+    }
+  }
+
+  // 搜索卡片
+  .search-card {
+    margin-bottom: 20px;
+
+    :deep(.el-card__body) {
+      padding: 20px 20px 0;
+    }
+
+    .el-form-item {
+      margin-bottom: 20px;
+    }
+  }
+
+  // 表格卡片
+  .table-card {
+    :deep(.el-card__body) {
+      padding: 0;
+    }
+
+    .table-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 20px;
+      border-bottom: 1px solid #ebeef5;
+
+      .table-title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+
+      .table-tools {
+        display: flex;
+        gap: 8px;
+      }
+    }
+
+    .el-table {
+      :deep(.el-table__header) {
+        th {
+          background-color: #f5f7fa;
+          color: #303133;
+          font-weight: 600;
+        }
+      }
+
+      :deep(.warning-row) {
+        background-color: #fef0f0;
+      }
+
+      :deep(.success-row) {
+        background-color: #f0f9ff;
+      }
+
+      .time-info {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 4px;
+        color: #606266;
+
+        .el-icon {
+          color: #909399;
+        }
+      }
+
+      .user-info {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 8px;
+
+        .el-avatar {
+          background-color: #409eff;
+          color: #fff;
+          font-size: 12px;
+        }
+      }
+
+      .text-muted {
+        color: #c0c4cc;
+      }
+    }
+  }
+
+  // 详情抽屉
+  .detail-drawer {
+    :deep(.el-drawer__header) {
+      margin-bottom: 0;
+      padding-bottom: 20px;
+      border-bottom: 1px solid #ebeef5;
+    }
+
+    .drawer-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+
+      h3 {
+        margin: 0;
+        font-size: 18px;
+        color: #303133;
+      }
+    }
+
+    .detail-content {
+      padding: 20px;
+
+      .detail-value {
+        font-weight: 600;
+        color: #409eff;
+      }
+
+      .attachment-section {
+        margin-top: 30px;
+        padding-top: 20px;
+        border-top: 1px solid #ebeef5;
+
+        h4 {
+          margin: 0 0 15px 0;
+          font-size: 16px;
+          color: #303133;
+        }
+      }
+    }
+  }
+
+  // 对话框样式
+  :deep(.el-dialog) {
+    .el-dialog__header {
+      background-color: #f5f7fa;
+      padding: 20px;
+
+      .el-dialog__title {
+        font-size: 18px;
+        color: #303133;
+      }
+    }
+
+    .el-dialog__body {
+      padding: 20px;
+    }
+
+    .el-form {
+      .el-form-item__label {
+        font-weight: 500;
+      }
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .repair-order-container {
+    padding: 10px;
+
+    .page-header {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 10px;
+
+      .header-actions {
+        width: 100%;
+        display: flex;
+        justify-content: flex-end;
+      }
+    }
+
+    .statistics-cards {
+      .el-col {
+        margin-bottom: 10px;
+      }
+    }
+
+    .search-card {
+      :deep(.el-form) {
+        .el-form-item {
+          display: block;
+          margin-bottom: 16px;
+        }
+      }
+    }
+
+    .detail-drawer {
+      :deep(.el-drawer) {
+        width: 90% !important;
+      }
+    }
+  }
+}
+
+// 动画效果
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.stat-card {
+  animation: fadeIn 0.5s ease-out;
+
+  &:nth-child(1) { animation-delay: 0.1s; }
+  &:nth-child(2) { animation-delay: 0.2s; }
+  &:nth-child(3) { animation-delay: 0.3s; }
+  &:nth-child(4) { animation-delay: 0.4s; }
+}
+
+// 标签样式优化
+:deep(.el-tag) {
+  .el-icon {
+    margin-right: 4px;
+  }
+}
+
+// 链接样式
+.el-link {
+  font-weight: 500;
+}
+
+// 分页器样式
+:deep(.pagination-container) {
+  padding: 20px;
+  background: #fff;
+}
+</style>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است