coder-common-thin-frontend/src/views/monitor/redis/components/CommandStatsPieChart.vue
Leo a361b7f7a0 feat(监控): 实现Redis监控页面
- 新增Redis监控主页面,展示Redis服务器状态和性能指标
- 实现命令统计饼图组件,支持SVG绘制和交互悬停效果
- 实现内存消耗仪表盘组件,采用圆环进度条设计
- 智能处理Redis无限制内存配置的显示逻辑
- 集成Redis基本信息展示和实时数据刷新功能
2025-09-28 00:09:19 +08:00

333 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="pie-chart-container">
<div v-if="!data || data.length === 0" class="empty-state">
<n-empty description="暂无命令统计数据">
<template #icon>
<n-icon><IconParkOutlineChartPie /></n-icon>
</template>
</n-empty>
</div>
<div v-else class="chart-content">
<!-- SVG饼图 -->
<svg class="pie-chart" :width="chartSize" :height="chartSize" :viewBox="`0 0 ${chartSize} ${chartSize}`">
<!-- 饼图扇形 -->
<g :transform="`translate(${chartSize / 2}, ${chartSize / 2})`">
<path
v-for="(segment, index) in pieSegments"
:key="index"
:d="segment.path"
:fill="segment.color"
stroke="var(--card-color)"
:stroke-width="2"
class="pie-segment"
@mouseenter="hoveredIndex = index"
@mouseleave="hoveredIndex = -1"
/>
</g>
<!-- 中心文本 -->
<g :transform="`translate(${chartSize / 2}, ${chartSize / 2})`">
<text
class="center-text-title"
text-anchor="middle"
:y="-8"
fill="var(--text-color-1)"
>
命令统计
</text>
<text
class="center-text-value"
text-anchor="middle"
:y="12"
fill="var(--text-color-2)"
>
{{ totalCommands.toLocaleString() }}
</text>
</g>
</svg>
<!-- 图例 -->
<div class="chart-legend">
<div
v-for="(item, index) in topCommands"
:key="index"
class="legend-item"
:class="{ active: hoveredIndex === index }"
@mouseenter="hoveredIndex = index"
@mouseleave="hoveredIndex = -1"
>
<div class="legend-color" :style="{ backgroundColor: colors[index] }" />
<div class="legend-content">
<div class="legend-name">
{{ item.name }}
</div>
<div class="legend-value">
{{ Number(item.value).toLocaleString() }} ({{ item.percentage }}%)
</div>
</div>
</div>
<div v-if="otherCommands > 0" class="legend-item">
<div class="legend-color" :style="{ backgroundColor: colors[topCommands.length] }" />
<div class="legend-content">
<div class="legend-name">
其他
</div>
<div class="legend-value">
{{ otherCommands.toLocaleString() }} ({{ otherPercentage }}%)
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { RedisCommandStatVo } from '@/service/api/monitor/redis'
// Props
interface Props {
data: RedisCommandStatVo[]
}
const props = defineProps<Props>()
// 响应式数据
const hoveredIndex = ref(-1)
const chartSize = 240
const radius = 80
// 颜色配置
const colors = [
'#18a058',
'#2080f0',
'#f0a020',
'#d03050',
'#722ed1',
'#eb2f96',
'#fa8c16',
'#52c41a',
'#13c2c2',
'#1890ff',
]
// 计算属性
const totalCommands = computed(() => {
return props.data.reduce((sum, item) => sum + Number(item.value), 0)
})
// 取前8个命令其余归为"其他"
const topCommands = computed(() => {
const sorted = [...props.data]
.sort((a, b) => Number(b.value) - Number(a.value))
.slice(0, 8)
return sorted.map(item => ({
...item,
percentage: totalCommands.value > 0
? ((Number(item.value) / totalCommands.value) * 100).toFixed(1)
: '0.0',
}))
})
const otherCommands = computed(() => {
if (props.data.length <= 8)
return 0
const topTotal = topCommands.value.reduce((sum, item) => sum + Number(item.value), 0)
return totalCommands.value - topTotal
})
const otherPercentage = computed(() => {
if (otherCommands.value === 0 || totalCommands.value === 0)
return '0.0'
return ((otherCommands.value / totalCommands.value) * 100).toFixed(1)
})
// 饼图扇形数据
const pieSegments = computed(() => {
const segments = []
let currentAngle = -Math.PI / 2 // 从12点钟方向开始
// 添加主要命令扇形
topCommands.value.forEach((item, index) => {
const percentage = Number(item.value) / totalCommands.value
const angle = percentage * 2 * Math.PI
if (angle > 0) {
const path = createArcPath(currentAngle, currentAngle + angle, radius)
segments.push({
path,
color: colors[index],
percentage: item.percentage,
})
currentAngle += angle
}
})
// 添加"其他"扇形
if (otherCommands.value > 0) {
const percentage = otherCommands.value / totalCommands.value
const angle = percentage * 2 * Math.PI
if (angle > 0) {
const path = createArcPath(currentAngle, currentAngle + angle, radius)
segments.push({
path,
color: colors[topCommands.value.length],
percentage: otherPercentage.value,
})
}
}
return segments
})
// 创建弧形路径
function createArcPath(startAngle: number, endAngle: number, radius: number): string {
const x1 = Math.cos(startAngle) * radius
const y1 = Math.sin(startAngle) * radius
const x2 = Math.cos(endAngle) * radius
const y2 = Math.sin(endAngle) * radius
const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0
return [
'M',
0,
0,
'L',
x1,
y1,
'A',
radius,
radius,
0,
largeArcFlag,
1,
x2,
y2,
'Z',
].join(' ')
}
</script>
<style scoped>
.pie-chart-container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.empty-state {
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-content {
display: flex;
align-items: center;
gap: 32px;
width: 100%;
max-width: 600px;
justify-content: center;
}
.pie-chart {
flex-shrink: 0;
}
.pie-segment {
cursor: pointer;
transition: all 0.3s ease;
}
.pie-segment:hover {
filter: brightness(1.1);
transform: scale(1.02);
}
.center-text-title {
font-size: 14px;
font-weight: 500;
}
.center-text-value {
font-size: 18px;
font-weight: 600;
}
.chart-legend {
flex: 1;
max-height: 280px;
overflow-y: auto;
padding-right: 8px;
}
.legend-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s ease;
cursor: pointer;
}
.legend-item:hover,
.legend-item.active {
background-color: var(--hover-color);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-content {
flex: 1;
min-width: 0;
}
.legend-name {
font-size: 14px;
font-weight: 500;
color: var(--text-color-1);
margin-bottom: 2px;
text-transform: uppercase;
}
.legend-value {
font-size: 12px;
color: var(--text-color-3);
}
/* 滚动条样式 */
.chart-legend::-webkit-scrollbar {
width: 4px;
}
.chart-legend::-webkit-scrollbar-track {
background: var(--scrollbar-track-color);
border-radius: 2px;
}
.chart-legend::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-color);
border-radius: 2px;
}
.chart-legend::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover-color);
}
</style>