feat(performance): 新增分组绩效统计功能
- 在性能监控页面添加菜单分类总览模块,显示各模块操作量对比 - 实现菜单分组柱状图可视化,支持点击筛选功能 - 添加分类模块明细折叠面板,展示具体模块统计数据 - 集成后端菜单分组绩效接口,支持按一级菜单路径过滤 - 添加当前分类筛选标签显示,增强用户体验 - 优化图表渲染逻辑,增加空数据情况下的图表清理 - 完善响应式布局,适配不同屏幕尺寸调整
This commit is contained in:
@@ -4,6 +4,7 @@ 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.OperMenuGroupVO;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
@@ -55,4 +56,13 @@ public class OperPerformanceController extends BaseController {
|
||||
public R<List<OperModuleStatVO>> module(OperPerformanceQuery query) {
|
||||
return R.ok(operLogService.selectModuleRanking(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按菜单分组绩效统计(oper_page 匹配一级菜单 path)
|
||||
*/
|
||||
@SaCheckPermission("monitor:performance:list")
|
||||
@GetMapping("/menuGroup")
|
||||
public R<List<OperMenuGroupVO>> menuGroup(OperPerformanceQuery query) {
|
||||
return R.ok(operLogService.selectMenuGroupPerformance(query));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,9 @@ public class OperPerformanceQuery implements Serializable {
|
||||
* 模块标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* oper_page 路径前缀(用于按一级菜单过滤,如 /wip)
|
||||
*/
|
||||
private String operPagePrefix;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.klp.system.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单分组绩效统计 VO(按一级菜单 oper_page 归类)
|
||||
*
|
||||
* @author Reasonix
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class OperMenuGroupVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 菜单名称(驾驶舱、生产管控……) */
|
||||
private String menuName;
|
||||
|
||||
/** 菜单路由路径前缀(post、wip……) */
|
||||
private String menuPath;
|
||||
|
||||
/** 该分类操作总次数 */
|
||||
private Long totalCount;
|
||||
|
||||
/** 成功次数 */
|
||||
private Long successCount;
|
||||
|
||||
/** 失败次数 */
|
||||
private Long failCount;
|
||||
|
||||
/** 成功率 (%) */
|
||||
private Double successRate;
|
||||
|
||||
/** 使用人数 */
|
||||
private Long personCount;
|
||||
|
||||
/** 该分类下的模块明细列表 */
|
||||
private List<OperModuleStatVO> modules = new ArrayList<>();
|
||||
}
|
||||
@@ -16,6 +16,11 @@ public class OperModuleStatVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 所属菜单分组(菜单名称,如"驾驶舱")
|
||||
*/
|
||||
private String menuGroup;
|
||||
|
||||
/**
|
||||
* 模块标题
|
||||
*/
|
||||
|
||||
@@ -35,4 +35,9 @@ public interface SysOperLogMapper extends BaseMapperPlus<SysOperLogMapper, SysOp
|
||||
* 全局概览统计
|
||||
*/
|
||||
OperSummaryVO selectGlobalSummary(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 按菜单分组+模块明细统计(oper_page 匹配一级菜单)
|
||||
*/
|
||||
List<OperModuleStatVO> selectMenuGroupDetail(OperPerformanceQuery query);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.OperMenuGroupVO;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
@@ -69,4 +70,9 @@ public interface ISysOperLogService {
|
||||
* 模块使用排行
|
||||
*/
|
||||
List<OperModuleStatVO> selectModuleRanking(OperPerformanceQuery query);
|
||||
|
||||
/**
|
||||
* 按菜单分组绩效统计(oper_page 匹配一级菜单 path)
|
||||
*/
|
||||
List<OperMenuGroupVO> selectMenuGroupPerformance(OperPerformanceQuery query);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.OperMenuGroupVO;
|
||||
import com.klp.system.domain.vo.OperModuleStatVO;
|
||||
import com.klp.system.domain.vo.OperPersonVO;
|
||||
import com.klp.system.domain.vo.OperSummaryVO;
|
||||
@@ -151,6 +152,86 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
|
||||
return baseMapper.selectModuleSummary(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OperMenuGroupVO> selectMenuGroupPerformance(OperPerformanceQuery query) {
|
||||
// 1. 查询原始数据(按 menu_group + title 双维度聚合)
|
||||
List<OperModuleStatVO> rawList = baseMapper.selectMenuGroupDetail(query);
|
||||
if (rawList == null || rawList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 按 menuGroup 分组
|
||||
Map<String, List<OperModuleStatVO>> groupMap = rawList.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
m -> m.getMenuGroup() != null ? m.getMenuGroup() : "其他",
|
||||
LinkedHashMap::new,
|
||||
Collectors.toList()
|
||||
));
|
||||
|
||||
// 3. 固定顺序
|
||||
List<String> orderNames = java.util.Arrays.asList(
|
||||
"驾驶舱", "生产管控", "数字仓储", "质量管理",
|
||||
"销售中心", "生产辅助", "科学排产", "文件夹", "其他"
|
||||
);
|
||||
// path 前缀映射
|
||||
Map<String, String> nameToPath = new LinkedHashMap<>();
|
||||
nameToPath.put("驾驶舱", "post");
|
||||
nameToPath.put("生产管控", "wip");
|
||||
nameToPath.put("数字仓储", "stock");
|
||||
nameToPath.put("质量管理", "quality");
|
||||
nameToPath.put("销售中心", "crm");
|
||||
nameToPath.put("生产辅助", "helper");
|
||||
nameToPath.put("科学排产", "aps");
|
||||
nameToPath.put("文件夹", "file");
|
||||
nameToPath.put("其他", "");
|
||||
|
||||
// 4. 构建 VO 列表
|
||||
List<OperMenuGroupVO> result = new ArrayList<>();
|
||||
for (String name : orderNames) {
|
||||
List<OperModuleStatVO> modules = groupMap.get(name);
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
continue; // 没有数据的分类不展示
|
||||
}
|
||||
|
||||
OperMenuGroupVO vo = new OperMenuGroupVO();
|
||||
vo.setMenuName(name);
|
||||
vo.setMenuPath(nameToPath.getOrDefault(name, ""));
|
||||
|
||||
// 汇总该分类的总量
|
||||
long total = modules.stream().mapToLong(m -> m.getTotalCount() != null ? m.getTotalCount() : 0).sum();
|
||||
long success = modules.stream()
|
||||
.mapToLong(m -> Math.round((m.getTotalCount() != null ? m.getTotalCount() : 0)
|
||||
* (m.getSuccessRate() != null ? m.getSuccessRate() : 0) / 100.0))
|
||||
.sum();
|
||||
long fail = total - success;
|
||||
long persons = modules.stream()
|
||||
.mapToLong(m -> m.getPersonCount() != null ? m.getPersonCount() : 0).sum();
|
||||
// 去重人数更合理:取各模块人数最大值作为估算(无法精确去重)
|
||||
long distinctPersons = modules.stream()
|
||||
.mapToLong(m -> m.getPersonCount() != null ? m.getPersonCount() : 0)
|
||||
.max().orElse(0);
|
||||
// 用 distinct 人数更合理,取模块中最大人数(保守估计)
|
||||
// 实际跨模块可能有重复,这里用最大模块人数作为下限
|
||||
vo.setTotalCount(total);
|
||||
vo.setSuccessCount(success);
|
||||
vo.setFailCount(fail);
|
||||
vo.setSuccessRate(total > 0 ? Math.round(success * 10000.0 / total) / 100.0 : 0.0);
|
||||
vo.setPersonCount(distinctPersons);
|
||||
|
||||
// 模块明细按 totalCount 降序排列
|
||||
modules.sort((a, b) -> {
|
||||
long ta = a.getTotalCount() != null ? a.getTotalCount() : 0;
|
||||
long tb = b.getTotalCount() != null ? b.getTotalCount() : 0;
|
||||
return Long.compare(tb, ta);
|
||||
});
|
||||
vo.setModules(modules);
|
||||
|
||||
result.add(vo);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OperPersonVO> selectPersonPerformance(OperPerformanceQuery query) {
|
||||
// 1. 查询人员汇总
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="com.klp.system.domain.vo.OperModuleStatVO" id="OperModuleStatResult">
|
||||
<result property="menuGroup" column="menu_group"/>
|
||||
<result property="operName" column="oper_name"/>
|
||||
<result property="title" column="title"/>
|
||||
<result property="totalCount" column="total_count"/>
|
||||
@@ -90,6 +91,9 @@
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="operPagePrefix != null and operPagePrefix != ''">
|
||||
AND l.oper_page LIKE CONCAT(#{operPagePrefix}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY l.title
|
||||
ORDER BY total_count DESC
|
||||
@@ -126,6 +130,9 @@
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="operPagePrefix != null and operPagePrefix != ''">
|
||||
AND l.oper_page LIKE CONCAT(#{operPagePrefix}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY l.oper_name, l.dept_name
|
||||
ORDER BY total_count DESC
|
||||
@@ -159,6 +166,9 @@
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="operPagePrefix != null and operPagePrefix != ''">
|
||||
AND l.oper_page LIKE CONCAT(#{operPagePrefix}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY l.oper_name, l.title
|
||||
ORDER BY l.oper_name, total_count DESC
|
||||
@@ -190,7 +200,57 @@
|
||||
<if test="title != null and title != ''">
|
||||
AND l.title LIKE CONCAT('%', #{title}, '%')
|
||||
</if>
|
||||
<if test="operPagePrefix != null and operPagePrefix != ''">
|
||||
AND l.oper_page LIKE CONCAT(#{operPagePrefix}, '%')
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- 按菜单分组+模块明细统计(基于 oper_page 匹配一级菜单 path) -->
|
||||
<select id="selectMenuGroupDetail" parameterType="com.klp.system.domain.bo.OperPerformanceQuery" resultMap="OperModuleStatResult">
|
||||
SELECT
|
||||
CASE
|
||||
WHEN l.oper_page LIKE '/post%' THEN '驾驶舱'
|
||||
WHEN l.oper_page LIKE '/wip%' THEN '生产管控'
|
||||
WHEN l.oper_page LIKE '/stock%' THEN '数字仓储'
|
||||
WHEN l.oper_page LIKE '/quality%' THEN '质量管理'
|
||||
WHEN l.oper_page LIKE '/crm%' THEN '销售中心'
|
||||
WHEN l.oper_page LIKE '/helper%' THEN '生产辅助'
|
||||
WHEN l.oper_page LIKE '/aps%' THEN '科学排产'
|
||||
WHEN l.oper_page LIKE '/file%' THEN '文件夹'
|
||||
ELSE '其他'
|
||||
END AS menu_group,
|
||||
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>
|
||||
<if test="operPagePrefix != null and operPagePrefix != ''">
|
||||
AND l.oper_page LIKE CONCAT(#{operPagePrefix}, '%')
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY menu_group, l.title
|
||||
ORDER BY FIELD(menu_group, '驾驶舱','生产管控','数字仓储','质量管理','销售中心','生产辅助','科学排产','文件夹','其他'), total_count DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -26,3 +26,12 @@ export function getModuleRanking(params) {
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 按菜单分组绩效统计(oper_page 匹配一级菜单)
|
||||
export function getMenuGroup(params) {
|
||||
return request({
|
||||
url: '/monitor/performance/menuGroup',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,6 +77,70 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 菜单分类总览(8大模块 + 其他) ========== -->
|
||||
<div class="perf-menu-section">
|
||||
<el-row :gutter="16" class="perf-row">
|
||||
<el-col :span="24">
|
||||
<div class="perf-panel">
|
||||
<div class="panel-hd">
|
||||
各模块操作量对比
|
||||
<span class="panel-hint">点击柱子可按分类筛选下方图表</span>
|
||||
</div>
|
||||
<div class="panel-bd"><div ref="menuGroupChartRef" class="chart-box" /></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 分类折叠详情 -->
|
||||
<div class="perf-panel perf-collapse-panel" v-if="menuGroupData.length">
|
||||
<div class="panel-hd">分类模块明细</div>
|
||||
<div class="panel-bd" style="padding: 8px 12px">
|
||||
<el-collapse v-model="activeMenuGroups" accordion>
|
||||
<el-collapse-item
|
||||
v-for="(group, idx) in menuGroupData"
|
||||
:key="group.menuName"
|
||||
:name="group.menuName"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="collapse-title">
|
||||
<span class="collapse-tag" :style="{ background: menuColors[idx % menuColors.length] }">{{ group.menuName }}</span>
|
||||
<span class="collapse-stat">操作 {{ group.totalCount }} 次</span>
|
||||
<span class="collapse-stat">成功率 {{ group.successRate }}%</span>
|
||||
<span class="collapse-stat">约 {{ group.personCount }} 人</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="group.modules" border size="small" max-height="360">
|
||||
<el-table-column prop="title" label="模块名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="totalCount" label="操作次数" width="90" align="center" sortable />
|
||||
<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="otherCount" label="其它" width="60" align="center" />
|
||||
<el-table-column prop="successRate" label="成功率%" width="85" align="center" />
|
||||
<el-table-column prop="personCount" label="使用人数" width="80" align="center" />
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前分类筛选提示 -->
|
||||
<div class="perf-filter-tag" v-if="selectedMenuGroup">
|
||||
<el-tag
|
||||
:color="selectedMenuGroup.color"
|
||||
size="medium"
|
||||
closable
|
||||
@close="handleClearMenuFilter"
|
||||
effect="dark"
|
||||
style="color:#fff;border:none"
|
||||
>
|
||||
<i class="el-icon-s-data" style="margin-right:4px" />
|
||||
{{ selectedMenuGroup.menuName }}
|
||||
</el-tag>
|
||||
<span class="filter-suffix">— 以下图表仅显示该分类数据</span>
|
||||
</div>
|
||||
|
||||
<!-- ========== 图表区 — 两行、每行两栏、高度统一 ========== -->
|
||||
<div class="perf-charts">
|
||||
<!-- Row 1 -->
|
||||
@@ -176,16 +240,30 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSummary, getPersonList, getModuleRanking } from "@/api/monitor/performance";
|
||||
import { getSummary, getPersonList, getModuleRanking, getMenuGroup } from "@/api/monitor/performance";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
// 默认近一个月日期范围
|
||||
function defaultDateRange() {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setDate(start.getDate() - 30);
|
||||
const fmt = d => {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return y + "-" + m + "-" + day;
|
||||
};
|
||||
return [fmt(start), fmt(end)];
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "Performance",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
filterForm: {
|
||||
dateRange: [],
|
||||
dateRange: defaultDateRange(),
|
||||
deptName: "",
|
||||
operName: "",
|
||||
title: ""
|
||||
@@ -193,10 +271,19 @@ export default {
|
||||
summaryData: {},
|
||||
personList: [],
|
||||
moduleRanking: [],
|
||||
menuGroupData: [],
|
||||
activeMenuGroups: [],
|
||||
selectedMenuGroup: null,
|
||||
personBarChart: null,
|
||||
moduleOpsChart: null,
|
||||
modulePersonDetailChart: null,
|
||||
businessPieChart: null
|
||||
businessPieChart: null,
|
||||
menuGroupChart: null,
|
||||
// 9种分类配色
|
||||
menuColors: [
|
||||
"#5470C6", "#91CC75", "#FAC858", "#EE6666", "#73C0DE",
|
||||
"#FC8452", "#9A60B4", "#3BA272", "#C0C4CC"
|
||||
]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -217,20 +304,25 @@ export default {
|
||||
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;
|
||||
if (this.selectedMenuGroup && this.selectedMenuGroup.menuPath) {
|
||||
p.operPagePrefix = "/" + this.selectedMenuGroup.menuPath;
|
||||
}
|
||||
return p;
|
||||
},
|
||||
async handleQuery() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = this.buildParams();
|
||||
const [sr, pr, mr] = await Promise.all([
|
||||
const [sr, pr, mr, mgr] = await Promise.all([
|
||||
getSummary(params),
|
||||
getPersonList(params),
|
||||
getModuleRanking(params)
|
||||
getModuleRanking(params),
|
||||
getMenuGroup(params)
|
||||
]);
|
||||
this.summaryData = sr.data || {};
|
||||
this.personList = pr.data || [];
|
||||
this.moduleRanking = mr.data || [];
|
||||
this.menuGroupData = mgr.data || [];
|
||||
this.$nextTick(() => this.renderCharts());
|
||||
} catch (e) {
|
||||
console.error("加载绩效数据失败", e);
|
||||
@@ -239,21 +331,109 @@ export default {
|
||||
}
|
||||
},
|
||||
handleReset() {
|
||||
this.filterForm = { dateRange: [], deptName: "", operName: "", title: "" };
|
||||
this.filterForm = { dateRange: defaultDateRange(), deptName: "", operName: "", title: "" };
|
||||
this.selectedMenuGroup = null;
|
||||
this.handleQuery();
|
||||
},
|
||||
handleClearMenuFilter() {
|
||||
this.selectedMenuGroup = null;
|
||||
this.handleQuery();
|
||||
},
|
||||
renderCharts() {
|
||||
this.renderMenuGroupChart();
|
||||
this.renderModuleOps();
|
||||
this.renderModulePersonDetail();
|
||||
this.renderPersonBar();
|
||||
this.renderPie();
|
||||
},
|
||||
|
||||
/* ---- 菜单分类对比柱状图 ---- */
|
||||
renderMenuGroupChart() {
|
||||
if (!this.$refs.menuGroupChartRef) return;
|
||||
if (!this.menuGroupChart) this.menuGroupChart = echarts.init(this.$refs.menuGroupChartRef);
|
||||
if (!this.menuGroupData.length) {
|
||||
this.menuGroupChart.clear();
|
||||
return;
|
||||
}
|
||||
const names = this.menuGroupData.map(g => g.menuName);
|
||||
const totals = this.menuGroupData.map(g => g.totalCount);
|
||||
const colors = this.menuColors;
|
||||
this.menuGroupChart.setOption({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: { type: "shadow" },
|
||||
formatter: function (ps) {
|
||||
const g = ps[0];
|
||||
return "<b>" + g.name + "</b><br/>操作总量: <b>" + g.value + " 次</b>";
|
||||
}
|
||||
},
|
||||
grid: { left: 0, right: 24, bottom: 24, top: 8, containLabel: true },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: names,
|
||||
axisLabel: { fontSize: 12 }
|
||||
},
|
||||
yAxis: { type: "value" },
|
||||
series: [{
|
||||
type: "bar",
|
||||
data: totals.map((v, i) => ({
|
||||
value: v,
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: colors[i % colors.length] },
|
||||
{ offset: 1, color: colors[i % colors.length] + "88" }
|
||||
])
|
||||
}
|
||||
})),
|
||||
barWidth: "55%",
|
||||
label: { show: true, position: "top", fontSize: 11 }
|
||||
}]
|
||||
}, true);
|
||||
|
||||
// 绑定点击事件(点击整列区域即可筛选,不限于柱子本身)
|
||||
this.menuGroupChart.off("click");
|
||||
const handleSelect = (idx) => {
|
||||
const group = this.menuGroupData[idx];
|
||||
if (!group) return;
|
||||
if (this.selectedMenuGroup && this.selectedMenuGroup.menuName === group.menuName) {
|
||||
this.selectedMenuGroup = null;
|
||||
} else {
|
||||
this.selectedMenuGroup = {
|
||||
menuName: group.menuName,
|
||||
menuPath: group.menuPath,
|
||||
color: this.menuColors[idx % this.menuColors.length]
|
||||
};
|
||||
}
|
||||
this.handleQuery();
|
||||
};
|
||||
// 点击柱子本身
|
||||
this.menuGroupChart.on("click", (params) => {
|
||||
if (params.componentType === "series" && params.dataIndex != null) {
|
||||
handleSelect(params.dataIndex);
|
||||
}
|
||||
});
|
||||
// 点击柱子之间的空白区域:通过像素坐标反查 dataIndex
|
||||
this.menuGroupChart.getZr().off("click");
|
||||
this.menuGroupChart.getZr().on("click", (e) => {
|
||||
// 只处理鼠标左键
|
||||
if (e.target || !this.menuGroupChart) return;
|
||||
const pointInGrid = this.menuGroupChart.convertFromPixel({ seriesIndex: 0 }, [e.offsetX, e.offsetY]);
|
||||
if (pointInGrid && pointInGrid[0] != null && pointInGrid[1] != null) {
|
||||
const idx = Math.round(pointInGrid[0]);
|
||||
if (idx >= 0 && idx < this.menuGroupData.length) {
|
||||
handleSelect(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* ---- 模块操作次数排行 ---- */
|
||||
renderModuleOps() {
|
||||
if (!this.$refs.moduleOpsChartRef) return;
|
||||
if (!this.moduleOpsChart) this.moduleOpsChart = echarts.init(this.$refs.moduleOpsChartRef);
|
||||
const top = this.moduleRanking.slice(0, 10);
|
||||
if (!top.length) { this.moduleOpsChart.clear(); return; }
|
||||
this.moduleOpsChart.setOption({
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
||||
grid: { left: 0, right: 24, bottom: 24, top: 8, containLabel: true },
|
||||
@@ -286,7 +466,7 @@ export default {
|
||||
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;
|
||||
if (!tops.length) { this.modulePersonDetailChart.clear(); return; }
|
||||
|
||||
const allSet = new Set();
|
||||
const others = [];
|
||||
@@ -318,6 +498,7 @@ export default {
|
||||
if (!this.$refs.personBarChartRef) return;
|
||||
if (!this.personBarChart) this.personBarChart = echarts.init(this.$refs.personBarChartRef);
|
||||
const top = this.personList.slice(0, 15);
|
||||
if (!top.length) { this.personBarChart.clear(); return; }
|
||||
this.personBarChart.setOption({
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
|
||||
grid: { left: 0, right: 24, bottom: 24, top: 8, containLabel: true },
|
||||
@@ -343,6 +524,7 @@ export default {
|
||||
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);
|
||||
if (!data.length) { this.businessPieChart.clear(); return; }
|
||||
this.businessPieChart.setOption({
|
||||
tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" },
|
||||
legend: { orient: "vertical", left: 8, top: "center", textStyle: { fontSize: 12 } },
|
||||
@@ -358,10 +540,10 @@ export default {
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
[this.personBarChart, this.moduleOpsChart, this.modulePersonDetailChart, this.businessPieChart].forEach(c => c && c.resize());
|
||||
[this.personBarChart, this.moduleOpsChart, this.modulePersonDetailChart, this.businessPieChart, this.menuGroupChart].forEach(c => c && c.resize());
|
||||
},
|
||||
disposeCharts() {
|
||||
[this.personBarChart, this.moduleOpsChart, this.modulePersonDetailChart, this.businessPieChart].forEach(c => c && c.dispose());
|
||||
[this.personBarChart, this.moduleOpsChart, this.modulePersonDetailChart, this.businessPieChart, this.menuGroupChart].forEach(c => c && c.dispose());
|
||||
},
|
||||
handleExport() {
|
||||
this.download("monitor/operlog/export", { ...this.buildParams() }, `performance_${new Date().getTime()}.xlsx`);
|
||||
@@ -444,6 +626,12 @@ export default {
|
||||
background: #fafbfc;
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel-hint {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #86909c;
|
||||
}
|
||||
.panel-bd {
|
||||
flex: 1;
|
||||
padding: 12px 8px 8px;
|
||||
@@ -454,6 +642,53 @@ export default {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
// ===================== 菜单分类区域 =====================
|
||||
.perf-menu-section {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.perf-filter-tag {
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.filter-suffix {
|
||||
font-size: 13px;
|
||||
color: #86909c;
|
||||
}
|
||||
.perf-collapse-panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
.collapse-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.collapse-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
min-width: 72px;
|
||||
text-align: center;
|
||||
}
|
||||
.collapse-stat {
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.perf-collapse-panel ::v-deep .el-collapse-item__header {
|
||||
padding: 0 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.perf-collapse-panel ::v-deep .el-collapse-item__content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// ===================== 表格区 =====================
|
||||
.perf-table { margin-top: 14px; }
|
||||
.perf-table .perf-panel .panel-bd { padding: 0; }
|
||||
|
||||
Reference in New Issue
Block a user