feat(performance): 新增分组绩效统计功能

- 在性能监控页面添加菜单分类总览模块,显示各模块操作量对比
- 实现菜单分组柱状图可视化,支持点击筛选功能
- 添加分类模块明细折叠面板,展示具体模块统计数据
- 集成后端菜单分组绩效接口,支持按一级菜单路径过滤
- 添加当前分类筛选标签显示,增强用户体验
- 优化图表渲染逻辑,增加空数据情况下的图表清理
- 完善响应式布局,适配不同屏幕尺寸调整
This commit is contained in:
2026-07-09 11:44:04 +08:00
parent 49bb8e116f
commit 451c7afcfd
10 changed files with 469 additions and 9 deletions

View File

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