feat(monitor): 添加操作日志绩效统计功能
- 在SysOperLogService中新增绩效概览、人员绩效和模块排行接口 - 在SysOperLogMapper中添加模块统计、人员统计和全局概览查询方法 - 在SysOperLogMapper.xml中实现绩效相关的SQL查询和ResultMap - 在SysOperLogServiceImpl中实现绩效统计业务逻辑和评分算法 - 创建OperModuleStatVO、OperPersonVO和OperSummaryVO数据传输对象 - 新增OperPerformanceController提供绩效统计API接口 - 添加前端performance页面实现数据可视化展示和图表渲染
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package com.klp.web.controller.monitor;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.klp.common.core.controller.BaseController;
|
||||
import com.klp.common.core.domain.R;
|
||||
import com.klp.system.domain.bo.OperPerformanceQuery;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
import com.klp.system.service.ISysOperLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 绩效汇总
|
||||
*
|
||||
* @author Reasonix
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/monitor/performance")
|
||||
public class OperPerformanceController extends BaseController {
|
||||
|
||||
private final ISysOperLogService operLogService;
|
||||
|
||||
/**
|
||||
* 绩效概览统计
|
||||
*/
|
||||
@SaCheckPermission("monitor:performance:list")
|
||||
@GetMapping("/summary")
|
||||
public R<OperSummaryVO> summary(OperPerformanceQuery query) {
|
||||
return R.ok(operLogService.selectPerformanceSummary(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 人员绩效列表(含模块明细)
|
||||
*/
|
||||
@SaCheckPermission("monitor:performance:list")
|
||||
@GetMapping("/person")
|
||||
public R<List<OperPersonVO>> person(OperPerformanceQuery query) {
|
||||
return R.ok(operLogService.selectPersonPerformance(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块使用排行
|
||||
*/
|
||||
@SaCheckPermission("monitor:performance:list")
|
||||
@GetMapping("/module")
|
||||
public R<List<OperModuleStatVO>> module(OperPerformanceQuery query) {
|
||||
return R.ok(operLogService.selectModuleRanking(query));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.klp.system.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 操作日志绩效查询参数
|
||||
*
|
||||
* @author Reasonix
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class OperPerformanceQuery implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private String beginTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 操作人员
|
||||
*/
|
||||
private String operName;
|
||||
|
||||
/**
|
||||
* 模块标题
|
||||
*/
|
||||
private String title;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.klp.system.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 模块维度统计 VO
|
||||
*
|
||||
* @author Reasonix
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class OperModuleStatVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 模块标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 该模块操作总次数
|
||||
*/
|
||||
private Long totalCount;
|
||||
|
||||
/**
|
||||
* 新增次数 (businessType=1)
|
||||
*/
|
||||
private Long addCount;
|
||||
|
||||
/**
|
||||
* 修改次数 (businessType=2)
|
||||
*/
|
||||
private Long editCount;
|
||||
|
||||
/**
|
||||
* 删除次数 (businessType=3)
|
||||
*/
|
||||
private Long deleteCount;
|
||||
|
||||
/**
|
||||
* 其它次数 (businessType=0)
|
||||
*/
|
||||
private Long otherCount;
|
||||
|
||||
/**
|
||||
* 成功率 (成功次数/总次数 * 100)
|
||||
*/
|
||||
private Double successRate;
|
||||
|
||||
/**
|
||||
* 操作人员(用于人员-模块明细关联)
|
||||
*/
|
||||
private String operName;
|
||||
|
||||
/**
|
||||
* 使用该模块的人数
|
||||
*/
|
||||
private Long personCount;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.klp.system.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人员绩效汇总 VO(含模块明细)
|
||||
*
|
||||
* @author Reasonix
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@ExcelIgnoreUnannotated
|
||||
public class OperPersonVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 操作人员
|
||||
*/
|
||||
@ExcelProperty(value = "操作人员")
|
||||
private String operName;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
@ExcelProperty(value = "部门名称")
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 总操作次数
|
||||
*/
|
||||
@ExcelProperty(value = "总操作次数")
|
||||
private Long totalCount;
|
||||
|
||||
/**
|
||||
* 成功次数
|
||||
*/
|
||||
@ExcelProperty(value = "成功次数")
|
||||
private Long successCount;
|
||||
|
||||
/**
|
||||
* 失败次数
|
||||
*/
|
||||
@ExcelProperty(value = "失败次数")
|
||||
private Long failCount;
|
||||
|
||||
/**
|
||||
* 成功率 (%)
|
||||
*/
|
||||
@ExcelProperty(value = "成功率(%)")
|
||||
private Double successRate;
|
||||
|
||||
/**
|
||||
* 新增次数
|
||||
*/
|
||||
@ExcelProperty(value = "新增次数")
|
||||
private Long addCount;
|
||||
|
||||
/**
|
||||
* 修改次数
|
||||
*/
|
||||
@ExcelProperty(value = "修改次数")
|
||||
private Long editCount;
|
||||
|
||||
/**
|
||||
* 删除次数
|
||||
*/
|
||||
@ExcelProperty(value = "删除次数")
|
||||
private Long deleteCount;
|
||||
|
||||
/**
|
||||
* 其它次数
|
||||
*/
|
||||
@ExcelProperty(value = "其它次数")
|
||||
private Long otherCount;
|
||||
|
||||
/**
|
||||
* 综合评分
|
||||
*/
|
||||
@ExcelProperty(value = "综合评分")
|
||||
private Double score;
|
||||
|
||||
/**
|
||||
* 最近操作时间
|
||||
*/
|
||||
@ExcelProperty(value = "最近操作时间")
|
||||
private Date lastOperTime;
|
||||
|
||||
/**
|
||||
* 该人员的模块明细列表
|
||||
*/
|
||||
private List<OperModuleStatVO> moduleStats = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.klp.system.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 绩效概览卡片 VO
|
||||
*
|
||||
* @author Reasonix
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class OperSummaryVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 总操作量
|
||||
*/
|
||||
private Long totalOperations;
|
||||
|
||||
/**
|
||||
* 成功操作数
|
||||
*/
|
||||
private Long successCount;
|
||||
|
||||
/**
|
||||
* 失败操作数
|
||||
*/
|
||||
private Long failCount;
|
||||
|
||||
/**
|
||||
* 活跃人数
|
||||
*/
|
||||
private Long activePersonCount;
|
||||
|
||||
/**
|
||||
* 活跃模块数
|
||||
*/
|
||||
private Long activeModuleCount;
|
||||
|
||||
/**
|
||||
* 人均操作数
|
||||
*/
|
||||
private Double avgPerPerson;
|
||||
}
|
||||
@@ -2,6 +2,12 @@ package com.klp.system.mapper;
|
||||
|
||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||
import com.klp.system.domain.SysOperLog;
|
||||
import com.klp.system.domain.bo.OperPerformanceQuery;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 操作日志 数据层
|
||||
@@ -10,4 +16,23 @@ import com.klp.system.domain.SysOperLog;
|
||||
*/
|
||||
public interface SysOperLogMapper extends BaseMapperPlus<SysOperLogMapper, SysOperLog, SysOperLog> {
|
||||
|
||||
/**
|
||||
* 模块全局统计
|
||||
*/
|
||||
List<OperModuleStatVO> selectModuleSummary(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 按人员统计
|
||||
*/
|
||||
List<OperPersonVO> selectPersonSummary(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 按人员-模块明细统计
|
||||
*/
|
||||
List<OperModuleStatVO> selectPersonModuleDetail(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 全局概览统计
|
||||
*/
|
||||
OperSummaryVO selectGlobalSummary(OperPerformanceQuery query);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ package com.klp.system.service;
|
||||
import com.klp.common.core.domain.PageQuery;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.system.domain.SysOperLog;
|
||||
import com.klp.system.domain.bo.OperPerformanceQuery;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -50,4 +54,19 @@ public interface ISysOperLogService {
|
||||
* 清空操作日志
|
||||
*/
|
||||
void cleanOperLog();
|
||||
|
||||
/**
|
||||
* 绩效概览统计
|
||||
*/
|
||||
OperSummaryVO selectPerformanceSummary(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 人员绩效列表(含模块明细)
|
||||
*/
|
||||
List<OperPersonVO> selectPersonPerformance(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 模块使用排行
|
||||
*/
|
||||
List<OperModuleStatVO> selectModuleRanking(OperPerformanceQuery query);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.common.utils.ip.AddressUtils;
|
||||
import com.klp.system.domain.SysOperLog;
|
||||
import com.klp.system.domain.bo.OperPerformanceQuery;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
import com.klp.system.mapper.SysOperLogMapper;
|
||||
import com.klp.system.service.ISysOperLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -17,10 +21,8 @@ import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 操作日志 服务层处理
|
||||
@@ -138,4 +140,64 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
|
||||
public void cleanOperLog() {
|
||||
baseMapper.delete(new LambdaQueryWrapper<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OperSummaryVO selectPerformanceSummary(OperPerformanceQuery query) {
|
||||
return baseMapper.selectGlobalSummary(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OperModuleStatVO> selectModuleRanking(OperPerformanceQuery query) {
|
||||
return baseMapper.selectModuleSummary(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OperPersonVO> selectPersonPerformance(OperPerformanceQuery query) {
|
||||
// 1. 查询人员汇总
|
||||
List<OperPersonVO> personList = baseMapper.selectPersonSummary(query);
|
||||
if (personList == null || personList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 查询人员-模块明细
|
||||
List<OperModuleStatVO> allModuleDetails = baseMapper.selectPersonModuleDetail(query);
|
||||
|
||||
// 3. 按人员分组模块明细
|
||||
Map<String, List<OperModuleStatVO>> moduleMap = Collections.emptyMap();
|
||||
if (allModuleDetails != null && !allModuleDetails.isEmpty()) {
|
||||
moduleMap = allModuleDetails.stream()
|
||||
.collect(Collectors.groupingBy(m -> m.getOperName() != null ? m.getOperName() : ""));
|
||||
}
|
||||
|
||||
// 4. 计算全局最大值用于评分归一化
|
||||
long maxTotalCount = personList.stream()
|
||||
.mapToLong(p -> p.getTotalCount() != null ? p.getTotalCount() : 0)
|
||||
.max().orElse(1);
|
||||
long maxModuleCount = moduleMap.values().stream()
|
||||
.mapToInt(List::size)
|
||||
.max().orElse(1);
|
||||
|
||||
// 5. 组装数据并计算综合评分
|
||||
for (OperPersonVO person : personList) {
|
||||
String name = person.getOperName();
|
||||
List<OperModuleStatVO> details = moduleMap.getOrDefault(name, Collections.emptyList());
|
||||
person.setModuleStats(details);
|
||||
|
||||
// 计算综合评分: 次数×40% + 成功率×30% + 模块覆盖度×30%
|
||||
double countScore = (person.getTotalCount() != null ? person.getTotalCount() : 0) * 1.0 / maxTotalCount * 40;
|
||||
double successScore = (person.getSuccessRate() != null ? person.getSuccessRate() : 0) / 100.0 * 30;
|
||||
double moduleScore = (maxModuleCount > 0 ? details.size() * 1.0 / maxModuleCount : 0) * 30;
|
||||
double score = Math.round((countScore + successScore + moduleScore) * 100.0) / 100.0;
|
||||
person.setScore(score);
|
||||
}
|
||||
|
||||
// 6. 按评分降序排序
|
||||
personList.sort((a, b) -> {
|
||||
double sa = a.getScore() != null ? a.getScore() : 0;
|
||||
double sb = b.getScore() != null ? b.getScore() : 0;
|
||||
return Double.compare(sb, sa);
|
||||
});
|
||||
|
||||
return personList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,173 @@
|
||||
<result property="operTime" column="oper_time"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 绩效相关 ResultMap -->
|
||||
<resultMap type="com.klp.system.domain.vo.OperPersonVO" id="OperPersonResult">
|
||||
<result property="operName" column="oper_name"/>
|
||||
<result property="deptName" column="dept_name"/>
|
||||
<result property="totalCount" column="total_count"/>
|
||||
<result property="successCount" column="success_count"/>
|
||||
<result property="failCount" column="fail_count"/>
|
||||
<result property="successRate" column="success_rate"/>
|
||||
<result property="addCount" column="add_count"/>
|
||||
<result property="editCount" column="edit_count"/>
|
||||
<result property="deleteCount" column="delete_count"/>
|
||||
<result property="otherCount" column="other_count"/>
|
||||
<result property="lastOperTime" column="last_oper_time"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="com.klp.system.domain.vo.OperModuleStatVO" id="OperModuleStatResult">
|
||||
<result property="operName" column="oper_name"/>
|
||||
<result property="title" column="title"/>
|
||||
<result property="totalCount" column="total_count"/>
|
||||
<result property="addCount" column="add_count"/>
|
||||
<result property="editCount" column="edit_count"/>
|
||||
<result property="deleteCount" column="delete_count"/>
|
||||
<result property="otherCount" column="other_count"/>
|
||||
<result property="successRate" column="success_rate"/>
|
||||
<result property="personCount" column="person_count"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="com.klp.system.domain.vo.OperSummaryVO" id="OperSummaryResult">
|
||||
<result property="totalOperations" column="total_operations"/>
|
||||
<result property="successCount" column="success_count"/>
|
||||
<result property="failCount" column="fail_count"/>
|
||||
<result property="activePersonCount" column="active_person_count"/>
|
||||
<result property="activeModuleCount" column="active_module_count"/>
|
||||
<result property="avgPerPerson" column="avg_per_person"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- ========== 绩效聚合查询 ========== -->
|
||||
|
||||
<!-- 按模块全局统计 -->
|
||||
<select id="selectModuleSummary" parameterType="com.klp.system.domain.bo.OperPerformanceQuery" resultMap="OperModuleStatResult">
|
||||
SELECT
|
||||
l.title,
|
||||
COUNT(1) AS total_count,
|
||||
SUM(CASE WHEN l.business_type = 1 THEN 1 ELSE 0 END) AS add_count,
|
||||
SUM(CASE WHEN l.business_type = 2 THEN 1 ELSE 0 END) AS edit_count,
|
||||
SUM(CASE WHEN l.business_type = 3 THEN 1 ELSE 0 END) AS delete_count,
|
||||
SUM(CASE WHEN l.business_type = 0 THEN 1 ELSE 0 END) AS other_count,
|
||||
ROUND(SUM(CASE WHEN l.status = 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(1), 2) AS success_rate,
|
||||
COUNT(DISTINCT l.oper_name) AS person_count
|
||||
FROM sys_oper_log l
|
||||
<where>
|
||||
<if test="beginTime != null and beginTime != ''">
|
||||
AND l.oper_time >= #{beginTime}
|
||||
</if>
|
||||
<if test="endTime != null and endTime != ''">
|
||||
AND l.oper_time <= #{endTime}
|
||||
</if>
|
||||
<if test="deptName != null and deptName != ''">
|
||||
AND l.dept_name LIKE CONCAT('%', #{deptName}, '%')
|
||||
</if>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND l.oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY l.title
|
||||
ORDER BY total_count DESC
|
||||
</select>
|
||||
|
||||
<!-- 按人员统计 -->
|
||||
<select id="selectPersonSummary" parameterType="com.klp.system.domain.bo.OperPerformanceQuery" resultMap="OperPersonResult">
|
||||
SELECT
|
||||
l.oper_name,
|
||||
l.dept_name,
|
||||
COUNT(1) AS total_count,
|
||||
SUM(CASE WHEN l.status = 0 THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN l.status = 1 THEN 1 ELSE 0 END) AS fail_count,
|
||||
ROUND(SUM(CASE WHEN l.status = 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(1), 2) AS success_rate,
|
||||
SUM(CASE WHEN l.business_type = 1 THEN 1 ELSE 0 END) AS add_count,
|
||||
SUM(CASE WHEN l.business_type = 2 THEN 1 ELSE 0 END) AS edit_count,
|
||||
SUM(CASE WHEN l.business_type = 3 THEN 1 ELSE 0 END) AS delete_count,
|
||||
SUM(CASE WHEN l.business_type = 0 THEN 1 ELSE 0 END) AS other_count,
|
||||
MAX(l.oper_time) AS last_oper_time
|
||||
FROM sys_oper_log l
|
||||
<where>
|
||||
<if test="beginTime != null and beginTime != ''">
|
||||
AND l.oper_time >= #{beginTime}
|
||||
</if>
|
||||
<if test="endTime != null and endTime != ''">
|
||||
AND l.oper_time <= #{endTime}
|
||||
</if>
|
||||
<if test="deptName != null and deptName != ''">
|
||||
AND l.dept_name LIKE CONCAT('%', #{deptName}, '%')
|
||||
</if>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND l.oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY l.oper_name, l.dept_name
|
||||
ORDER BY total_count DESC
|
||||
</select>
|
||||
|
||||
<!-- 按人员-模块明细统计 -->
|
||||
<select id="selectPersonModuleDetail" parameterType="com.klp.system.domain.bo.OperPerformanceQuery" resultMap="OperModuleStatResult">
|
||||
SELECT
|
||||
l.oper_name,
|
||||
l.title,
|
||||
COUNT(1) AS total_count,
|
||||
SUM(CASE WHEN l.business_type = 1 THEN 1 ELSE 0 END) AS add_count,
|
||||
SUM(CASE WHEN l.business_type = 2 THEN 1 ELSE 0 END) AS edit_count,
|
||||
SUM(CASE WHEN l.business_type = 3 THEN 1 ELSE 0 END) AS delete_count,
|
||||
SUM(CASE WHEN l.business_type = 0 THEN 1 ELSE 0 END) AS other_count,
|
||||
ROUND(SUM(CASE WHEN l.status = 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(1), 2) AS success_rate
|
||||
FROM sys_oper_log l
|
||||
<where>
|
||||
<if test="beginTime != null and beginTime != ''">
|
||||
AND l.oper_time >= #{beginTime}
|
||||
</if>
|
||||
<if test="endTime != null and endTime != ''">
|
||||
AND l.oper_time <= #{endTime}
|
||||
</if>
|
||||
<if test="deptName != null and deptName != ''">
|
||||
AND l.dept_name LIKE CONCAT('%', #{deptName}, '%')
|
||||
</if>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND l.oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY l.oper_name, l.title
|
||||
ORDER BY l.oper_name, total_count DESC
|
||||
</select>
|
||||
|
||||
<!-- 全局概览统计 -->
|
||||
<select id="selectGlobalSummary" parameterType="com.klp.system.domain.bo.OperPerformanceQuery" resultMap="OperSummaryResult">
|
||||
SELECT
|
||||
COUNT(1) AS total_operations,
|
||||
SUM(CASE WHEN l.status = 0 THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN l.status = 1 THEN 1 ELSE 0 END) AS fail_count,
|
||||
COUNT(DISTINCT l.oper_name) AS active_person_count,
|
||||
COUNT(DISTINCT l.title) AS active_module_count,
|
||||
ROUND(COUNT(1) * 1.0 / NULLIF(COUNT(DISTINCT l.oper_name), 0), 2) AS avg_per_person
|
||||
FROM sys_oper_log l
|
||||
<where>
|
||||
<if test="beginTime != null and beginTime != ''">
|
||||
AND l.oper_time >= #{beginTime}
|
||||
</if>
|
||||
<if test="endTime != null and endTime != ''">
|
||||
AND l.oper_time <= #{endTime}
|
||||
</if>
|
||||
<if test="deptName != null and deptName != ''">
|
||||
AND l.dept_name LIKE CONCAT('%', #{deptName}, '%')
|
||||
</if>
|
||||
<if test="operName != null and operName != ''">
|
||||
AND l.oper_name LIKE CONCAT('%', #{operName}, '%')
|
||||
</if>
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
28
klp-ui/src/api/monitor/performance.js
Normal file
28
klp-ui/src/api/monitor/performance.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 绩效概览统计
|
||||
export function getSummary(params) {
|
||||
return request({
|
||||
url: '/monitor/performance/summary',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 人员绩效列表(含模块明细)
|
||||
export function getPersonList(params) {
|
||||
return request({
|
||||
url: '/monitor/performance/person',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 模块使用排行
|
||||
export function getModuleRanking(params) {
|
||||
return request({
|
||||
url: '/monitor/performance/module',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
481
klp-ui/src/views/monitor/performance/index.vue
Normal file
481
klp-ui/src/views/monitor/performance/index.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div class="perf-container" v-loading="loading">
|
||||
<!-- ========== 筛选栏 ========== -->
|
||||
<div class="perf-filter">
|
||||
<el-form :inline="true" :model="filterForm" size="small" class="perf-filter-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="filterForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width:230px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-input v-model="filterForm.deptName" placeholder="部门" clearable style="width:130px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="人员">
|
||||
<el-input v-model="filterForm.operName" placeholder="人员" clearable style="width:130px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模块">
|
||||
<el-input v-model="filterForm.title" placeholder="模块" clearable style="width:130px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- ========== 六大指标卡 ========== -->
|
||||
<div class="perf-kpi-row">
|
||||
<div class="perf-kpi kpi-total">
|
||||
<div class="kpi-icon"><i class="el-icon-s-data" /></div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-val">{{ summaryData.totalOperations || 0 }}</div>
|
||||
<div class="kpi-lbl">总操作量</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-kpi kpi-ok">
|
||||
<div class="kpi-icon"><i class="el-icon-circle-check" /></div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-val">{{ summaryData.successCount || 0 }}</div>
|
||||
<div class="kpi-lbl">成功操作</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-kpi kpi-fail">
|
||||
<div class="kpi-icon"><i class="el-icon-circle-close" /></div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-val">{{ summaryData.failCount || 0 }}</div>
|
||||
<div class="kpi-lbl">失败操作</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-kpi kpi-user">
|
||||
<div class="kpi-icon"><i class="el-icon-user" /></div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-val">{{ summaryData.activePersonCount || 0 }}</div>
|
||||
<div class="kpi-lbl">活跃人数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-kpi kpi-mod">
|
||||
<div class="kpi-icon"><i class="el-icon-menu" /></div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-val">{{ summaryData.activeModuleCount || 0 }}</div>
|
||||
<div class="kpi-lbl">活跃模块</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-kpi kpi-avg">
|
||||
<div class="kpi-icon"><i class="el-icon-data-line" /></div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-val">{{ summaryData.avgPerPerson || 0 }}</div>
|
||||
<div class="kpi-lbl">人均操作</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 图表区 — 两行、每行两栏、高度统一 ========== -->
|
||||
<div class="perf-charts">
|
||||
<!-- Row 1 -->
|
||||
<el-row :gutter="16" class="perf-row">
|
||||
<el-col :span="10">
|
||||
<div class="perf-panel">
|
||||
<div class="panel-hd">模块操作排行 Top 10</div>
|
||||
<div class="panel-bd"><div ref="moduleOpsChartRef" class="chart-box" /></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="perf-panel">
|
||||
<div class="panel-hd">模块使用明细 — 谁用了哪个模块多少次</div>
|
||||
<div class="panel-bd"><div ref="modulePersonDetailRef" class="chart-box" /></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Row 2 -->
|
||||
<el-row :gutter="16" class="perf-row">
|
||||
<el-col :span="14">
|
||||
<div class="perf-panel">
|
||||
<div class="panel-hd">人员操作排行 Top 15</div>
|
||||
<div class="panel-bd"><div ref="personBarChartRef" class="chart-box" /></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<div class="perf-panel">
|
||||
<div class="panel-hd">业务类型分布</div>
|
||||
<div class="panel-bd"><div ref="businessPieChartRef" class="chart-box" /></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- ========== 人员明细表格 ========== -->
|
||||
<div class="perf-table">
|
||||
<div class="perf-panel">
|
||||
<div class="panel-hd">
|
||||
<span>人员绩效明细</span>
|
||||
<el-button
|
||||
style="float:right"
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
v-hasPermi="['monitor:performance:list']"
|
||||
>导出</el-button>
|
||||
</div>
|
||||
<div class="panel-bd" style="padding:0">
|
||||
<el-table :data="personList" border stripe row-key="operName" style="width:100%">
|
||||
<el-table-column type="expand">
|
||||
<template slot-scope="s">
|
||||
<div v-if="s.row.moduleStats && s.row.moduleStats.length" class="expand-box">
|
||||
<el-table :data="s.row.moduleStats" border size="mini">
|
||||
<el-table-column prop="title" label="模块" />
|
||||
<el-table-column prop="totalCount" label="次数" width="70" align="center" />
|
||||
<el-table-column prop="addCount" label="新增" width="55" align="center" />
|
||||
<el-table-column prop="editCount" label="修改" width="55" align="center" />
|
||||
<el-table-column prop="deleteCount" label="删除" width="55" align="center" />
|
||||
<el-table-column prop="otherCount" label="其它" width="55" align="center" />
|
||||
<el-table-column prop="successRate" label="成功率%" width="80" align="center" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else class="expand-empty">暂无模块明细</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operName" label="操作人员" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="deptName" label="部门" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="totalCount" label="总次数" width="80" align="center" sortable />
|
||||
<el-table-column prop="successCount" label="成功" width="65" align="center" />
|
||||
<el-table-column prop="failCount" label="失败" width="65" align="center" />
|
||||
<el-table-column prop="successRate" label="成功率%" width="85" align="center" />
|
||||
<el-table-column prop="addCount" label="新增" width="60" align="center" />
|
||||
<el-table-column prop="editCount" label="修改" width="60" align="center" />
|
||||
<el-table-column prop="deleteCount" label="删除" width="60" align="center" />
|
||||
<el-table-column prop="score" label="综合评分" width="90" align="center" sortable>
|
||||
<template slot-scope="s">
|
||||
<el-tag
|
||||
:type="s.row.score >= 80 ? 'success' : s.row.score >= 50 ? 'warning' : 'danger'"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>{{ s.row.score }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastOperTime" label="最近操作时间" width="160" align="center">
|
||||
<template slot-scope="s">
|
||||
<span>{{ parseTime(s.row.lastOperTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSummary, getPersonList, getModuleRanking } from "@/api/monitor/performance";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export default {
|
||||
name: "Performance",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
filterForm: {
|
||||
dateRange: [],
|
||||
deptName: "",
|
||||
operName: "",
|
||||
title: ""
|
||||
},
|
||||
summaryData: {},
|
||||
personList: [],
|
||||
moduleRanking: [],
|
||||
personBarChart: null,
|
||||
moduleOpsChart: null,
|
||||
modulePersonDetailChart: null,
|
||||
businessPieChart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.handleQuery();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
this.disposeCharts();
|
||||
},
|
||||
methods: {
|
||||
buildParams() {
|
||||
const p = {};
|
||||
if (this.filterForm.dateRange && this.filterForm.dateRange.length === 2) {
|
||||
p.beginTime = this.filterForm.dateRange[0] + " 00:00:00";
|
||||
p.endTime = this.filterForm.dateRange[1] + " 23:59:59";
|
||||
}
|
||||
if (this.filterForm.deptName) p.deptName = this.filterForm.deptName;
|
||||
if (this.filterForm.operName) p.operName = this.filterForm.operName;
|
||||
if (this.filterForm.title) p.title = this.filterForm.title;
|
||||
return p;
|
||||
},
|
||||
async handleQuery() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = this.buildParams();
|
||||
const [sr, pr, mr] = await Promise.all([
|
||||
getSummary(params),
|
||||
getPersonList(params),
|
||||
getModuleRanking(params)
|
||||
]);
|
||||
this.summaryData = sr.data || {};
|
||||
this.personList = pr.data || [];
|
||||
this.moduleRanking = mr.data || [];
|
||||
this.$nextTick(() => this.renderCharts());
|
||||
} catch (e) {
|
||||
console.error("加载绩效数据失败", e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleReset() {
|
||||
this.filterForm = { dateRange: [], deptName: "", operName: "", title: "" };
|
||||
this.handleQuery();
|
||||
},
|
||||
renderCharts() {
|
||||
this.renderModuleOps();
|
||||
this.renderModulePersonDetail();
|
||||
this.renderPersonBar();
|
||||
this.renderPie();
|
||||
},
|
||||
|
||||
/* ---- 模块操作次数排行 ---- */
|
||||
renderModuleOps() {
|
||||
if (!this.$refs.moduleOpsChartRef) return;
|
||||
if (!this.moduleOpsChart) this.moduleOpsChart = echarts.init(this.$refs.moduleOpsChartRef);
|
||||
const top = this.moduleRanking.slice(0, 10);
|
||||
this.moduleOpsChart.setOption({
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
||||
grid: { left: 0, right: 24, bottom: 24, top: 8, containLabel: true },
|
||||
xAxis: { type: "category", data: top.map(m => m.title), axisLabel: { rotate: 35, fontSize: 11 } },
|
||||
yAxis: { type: "value" },
|
||||
series: [{
|
||||
type: "bar", data: top.map(m => m.totalCount), barWidth: "55%",
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#409EFF" }, { offset: 1, color: "#a0cfff" }
|
||||
])
|
||||
},
|
||||
label: { show: true, position: "top", fontSize: 11 }
|
||||
}]
|
||||
}, true);
|
||||
},
|
||||
|
||||
/* ---- 模块-人员明细堆叠 ---- */
|
||||
renderModulePersonDetail() {
|
||||
if (!this.$refs.modulePersonDetailRef) return;
|
||||
if (!this.modulePersonDetailChart) this.modulePersonDetailChart = echarts.init(this.$refs.modulePersonDetailRef);
|
||||
const map = {};
|
||||
this.personList.forEach(p => {
|
||||
(p.moduleStats || []).forEach(m => {
|
||||
if (!map[m.title]) map[m.title] = {};
|
||||
map[m.title][p.operName] = (map[m.title][p.operName] || 0) + (m.totalCount || 0);
|
||||
});
|
||||
});
|
||||
const tops = Object.entries(map)
|
||||
.map(([t, pm]) => ({ title: t, personMap: pm, total: Object.values(pm).reduce((s, c) => s + c, 0) }))
|
||||
.sort((a, b) => b.total - a.total).slice(0, 6);
|
||||
if (!tops.length) return;
|
||||
|
||||
const allSet = new Set();
|
||||
const others = [];
|
||||
tops.forEach(mod => {
|
||||
const e = Object.entries(mod.personMap).sort((a, b) => b[1] - a[1]);
|
||||
mod._top5 = e.slice(0, 5);
|
||||
mod._other = e.slice(5).reduce((s, [, n]) => s + n, 0);
|
||||
mod._top5.forEach(([n]) => allSet.add(n));
|
||||
});
|
||||
const names = Array.from(allSet);
|
||||
const colors = ["#5470C6","#91CC75","#FAC858","#EE6666","#73C0DE","#FC8452","#9A60B4","#3BA272","#EA7CCC","#48C9B0","#D48265","#61A0A8","#CA8622","#BDA29A","#6E7074","#FF9F7F","#A5D8FF","#FFD666","#95DE64","#9E87FF"];
|
||||
const cmap = {}; names.forEach((n, i) => cmap[n] = colors[i % colors.length]);
|
||||
const ser = names.map(n => ({ name: n, type: "bar", stack: "a", emphasis: { focus: "series" }, data: tops.map(m => { const f = m._top5.find(([x]) => x === n); return f ? f[1] : 0; }), itemStyle: { color: cmap[n] } }));
|
||||
const ob = tops.map(m => m._other);
|
||||
if (ob.some(v => v > 0)) ser.push({ name: "其他", type: "bar", stack: "a", emphasis: { focus: "series" }, data: ob, itemStyle: { color: "#C0C4CC" } });
|
||||
|
||||
this.modulePersonDetailChart.setOption({
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, formatter(p) { let h = "<b>" + p[0].axisValue + "</b><br/>"; p.filter(x => x.value > 0).sort((a, b) => b.value - a.value).forEach(x => { h += '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px;background:' + x.color + '"/></span>'; h += x.seriesName + ": <b>" + x.value + "次</b><br/>"; }); return h; } },
|
||||
legend: { type: "scroll", bottom: 0, textStyle: { fontSize: 10 } },
|
||||
grid: { left: 0, right: 24, bottom: 48, top: 8, containLabel: true },
|
||||
xAxis: { type: "value" },
|
||||
yAxis: { type: "category", data: tops.map(m => m.title), axisLabel: { fontSize: 12 } },
|
||||
series: ser
|
||||
}, true);
|
||||
},
|
||||
|
||||
/* ---- 人员操作排行 ---- */
|
||||
renderPersonBar() {
|
||||
if (!this.$refs.personBarChartRef) return;
|
||||
if (!this.personBarChart) this.personBarChart = echarts.init(this.$refs.personBarChartRef);
|
||||
const top = this.personList.slice(0, 15);
|
||||
this.personBarChart.setOption({
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
||||
grid: { left: 0, right: 24, bottom: 24, top: 8, containLabel: true },
|
||||
xAxis: { type: "category", data: top.map(p => p.operName), axisLabel: { rotate: 35, fontSize: 11 } },
|
||||
yAxis: { type: "value" },
|
||||
series: [{
|
||||
type: "bar", data: top.map(p => p.totalCount || 0), barWidth: "50%",
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#5470C6" }, { offset: 1, color: "#b3c8ff" }
|
||||
])
|
||||
},
|
||||
label: { show: true, position: "top", fontSize: 11 }
|
||||
}]
|
||||
}, true);
|
||||
},
|
||||
|
||||
/* ---- 业务类型饼图 ---- */
|
||||
renderPie() {
|
||||
if (!this.$refs.businessPieChartRef) return;
|
||||
if (!this.businessPieChart) this.businessPieChart = echarts.init(this.$refs.businessPieChartRef);
|
||||
let a = 0, e = 0, d = 0, o = 0;
|
||||
this.personList.forEach(p => { a += p.addCount || 0; e += p.editCount || 0; d += p.deleteCount || 0; o += p.otherCount || 0; });
|
||||
const data = [{ value: a, name: "新增" }, { value: e, name: "修改" }, { value: d, name: "删除" }, { value: o, name: "其它" }].filter(x => x.value > 0);
|
||||
this.businessPieChart.setOption({
|
||||
tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" },
|
||||
legend: { orient: "vertical", left: 8, top: "center", textStyle: { fontSize: 12 } },
|
||||
series: [{
|
||||
type: "pie", radius: ["45%", "72%"], center: ["58%", "50%"],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },
|
||||
label: { show: true, formatter: "{b}\n{d}%" },
|
||||
emphasis: { label: { fontSize: 16, fontWeight: "bold" } },
|
||||
data
|
||||
}]
|
||||
}, true);
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
[this.personBarChart, this.moduleOpsChart, this.modulePersonDetailChart, this.businessPieChart].forEach(c => c && c.resize());
|
||||
},
|
||||
disposeCharts() {
|
||||
[this.personBarChart, this.moduleOpsChart, this.modulePersonDetailChart, this.businessPieChart].forEach(c => c && c.dispose());
|
||||
},
|
||||
handleExport() {
|
||||
this.download("monitor/operlog/export", { ...this.buildParams() }, `performance_${new Date().getTime()}.xlsx`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ===================== 容器 =====================
|
||||
.perf-container {
|
||||
padding: 16px 20px 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
// ===================== 筛选栏 =====================
|
||||
.perf-filter {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 14px 20px 2px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.04);
|
||||
}
|
||||
.perf-filter-form ::v-deep .el-form-item {
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// ===================== KPI 卡片行 =====================
|
||||
.perf-kpi-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.perf-kpi {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 18px 16px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.04);
|
||||
transition: box-shadow .2s;
|
||||
cursor: default;
|
||||
&:hover { box-shadow: 0 4px 14px rgba(0,0,0,.08); }
|
||||
}
|
||||
.kpi-icon {
|
||||
width: 44px; height: 44px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px; color: #fff; flex-shrink: 0; margin-right: 14px;
|
||||
}
|
||||
.kpi-body { overflow: hidden; }
|
||||
.kpi-val { font-size: 26px; font-weight: 700; line-height: 1.2; color: #1d2129; }
|
||||
.kpi-lbl { font-size: 12px; color: #86909c; margin-top: 2px; }
|
||||
|
||||
// 六色渐变
|
||||
.kpi-total .kpi-icon { background: linear-gradient(135deg, #5470C6, #8ba7f0); }
|
||||
.kpi-ok .kpi-icon { background: linear-gradient(135deg, #00b42a, #57d27a); }
|
||||
.kpi-fail .kpi-icon { background: linear-gradient(135deg, #f53f3f, #f98981); }
|
||||
.kpi-user .kpi-icon { background: linear-gradient(135deg, #0fc6c2, #5ce0db); }
|
||||
.kpi-mod .kpi-icon { background: linear-gradient(135deg, #ff7d00, #ffb566); }
|
||||
.kpi-avg .kpi-icon { background: linear-gradient(135deg, #722ed1, #b491e8); }
|
||||
|
||||
// ===================== 面板卡片 =====================
|
||||
.perf-row { margin-bottom: 14px; }
|
||||
.perf-panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.04);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.panel-hd {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px; font-weight: 600; color: #1d2129;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
background: #fafbfc;
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel-bd {
|
||||
flex: 1;
|
||||
padding: 12px 8px 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
// ===================== 表格区 =====================
|
||||
.perf-table { margin-top: 14px; }
|
||||
.perf-table .perf-panel .panel-bd { padding: 0; }
|
||||
|
||||
.expand-box {
|
||||
padding: 10px 28px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.expand-empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #c9cdd4;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 表格微调
|
||||
.perf-table ::v-deep .el-table th {
|
||||
background: #f7f8fa;
|
||||
font-weight: 600;
|
||||
color: #4e5969;
|
||||
}
|
||||
.perf-table ::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background: #fafbfc;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user