feat(monitor): 添加操作日志绩效统计功能

- 在SysOperLogService中新增绩效概览、人员绩效和模块排行接口
- 在SysOperLogMapper中添加模块统计、人员统计和全局概览查询方法
- 在SysOperLogMapper.xml中实现绩效相关的SQL查询和ResultMap
- 在SysOperLogServiceImpl中实现绩效统计业务逻辑和评分算法
- 创建OperModuleStatVO、OperPersonVO和OperSummaryVO数据传输对象
- 新增OperPerformanceController提供绩效统计API接口
- 添加前端performance页面实现数据可视化展示和图表渲染
This commit is contained in:
2026-07-01 15:43:26 +08:00
parent ad25227400
commit 9233d09edc
11 changed files with 1101 additions and 4 deletions

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<>();
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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 &gt;= #{beginTime}
</if>
<if test="endTime != null and endTime != ''">
AND l.oper_time &lt;= #{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 &gt;= #{beginTime}
</if>
<if test="endTime != null and endTime != ''">
AND l.oper_time &lt;= #{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 &gt;= #{beginTime}
</if>
<if test="endTime != null and endTime != ''">
AND l.oper_time &lt;= #{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 &gt;= #{beginTime}
</if>
<if test="endTime != null and endTime != ''">
AND l.oper_time &lt;= #{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>

View 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
})
}

View 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>