nazhua/src/views/components/server-detail/server-monitor.vue

580 lines
13 KiB
Vue

<template>
<dot-dot-box
v-if="monitorData.length"
class="server-monitor-group"
padding="16px 20px"
>
<div class="module-head-group">
<div class="left-box">
<span class="module-title">
网络监控
</span>
</div>
<div class="right-box">
<div
class="refresh-data-group"
title="是否自动刷新"
@click="switchRefresh"
>
<span class="label-text">刷新</span>
<div
class="switch-box"
:class="{
active: refreshData,
}"
>
<span class="switch-dot" />
</div>
</div>
<div
class="peak-shaving-group"
title="过滤太高或太低的数据"
@click="switchPeakShaving"
>
<span class="label-text">削峰</span>
<div
class="switch-box"
:class="{
active: peakShaving,
}"
>
<span class="switch-dot" />
</div>
</div>
<div class="last-update-time-group">
<span class="last-update-time-label">
最近
</span>
<div class="minutes">
<div
v-for="minuteItem in minutes"
:key="minuteItem.value"
class="minute-item"
:class="{
active: minuteItem.value === minute,
}"
@click="toggleMinute(minuteItem.value)"
>
<span>{{ minuteItem.label }}</span>
</div>
<div
class="active-arrow"
:style="minuteActiveArrowStyle"
/>
</div>
</div>
</div>
</div>
<div class="monitor-cate-group">
<div
v-for="cateItem in monitorChartData.cateList"
:key="cateItem.id"
class="monitor-cate-item"
:class="{
disabled: showCates[cateItem.id] === false,
}"
:style="{
'--cate-color': cateItem.color,
}"
:title="cateItem.title"
@click="toggleShowCate(cateItem.id)"
>
<span class="cate-legend" />
<span
class="cate-name"
>
{{ cateItem.name }}
</span>
<span
v-if="cateItem.avg !== 0"
class="cate-avg-ms"
>
{{ cateItem.avg }}ms
</span>
<span
v-else
class="cate-avg-ms"
>
-ms
</span>
</div>
</div>
<line-chart
:cate-list="monitorChartData.cateList"
:date-list="monitorChartData.dateList"
:value-list="monitorChartData.valueList"
/>
</dot-dot-box>
</template>
<script setup>
/**
* 服务器监控
*/
import {
ref,
computed,
onMounted,
onUnmounted,
} from 'vue';
import { useStore } from 'vuex';
import config from '@/config';
import request from '@/utils/request';
import validate from '@/utils/validate';
import LineChart from '@/components/charts/line.vue';
import {
getThreshold,
getLineColor,
} from '@/views/composable/server-monitor';
const props = defineProps({
info: {
type: Object,
default: () => ({}),
},
});
const store = useStore();
const minute = ref(1440);
const minutes = [{
label: '30分钟',
value: 30,
}, {
label: '1小时',
value: 60,
}, {
label: '3小时',
value: 180,
}, {
label: '6小时',
value: 360,
}, {
label: '12小时',
value: 720,
}, {
label: '24小时',
value: 1440,
}];
const refreshData = ref(true);
const peakShaving = ref(false);
const showCates = ref({});
const monitorData = ref([]);
const accpetShowTime = computed(() => {
const now = store.state.serverTime || Date.now();
return now - (minute.value * 60 * 1000);
});
const minuteActiveArrowStyle = computed(() => {
const index = minutes.findIndex((i) => i.value === minute.value);
return {
left: `calc(${index} * var(--minute-item-width))`,
};
});
const monitorChartData = computed(() => {
/**
* 处理监控数据以生成分类的平均延迟随时间变化的列表。
*
* @returns {Object} 返回一个对象,包含:
* - cateList {Array}: 唯一监控名称的列表。
* - dateList {Array}: 排序后的唯一时间戳列表。
* - valueList {Array}: 包含以下内容的对象列表:
* - name {String}: 监控名称。
* - data {Array}: [时间戳, 平均延迟] 对的数组。
*
* 该函数执行以下步骤:
* 1. 遍历监控数据以分类和过滤平均延迟。
* 2. 如果启用了削峰,则应用削峰以过滤异常值。
* 3. 构建监控名称到其各自时间戳和平均延迟的映射。
* 4. 将映射转换为监控名称、时间戳和平均延迟数据的列表。
* 5. 删除重复的时间戳并对其进行排序。
*/
const cateMap = {};
monitorData.value.forEach((i) => {
const dateMap = {};
if (!cateMap[i.monitor_name]) {
cateMap[i.monitor_name] = {
id: i.monitor_id,
dateMap,
avgs: [],
};
}
const showAvgDelay = [];
const showCreateTime = i.created_at.filter((o, index) => {
const status = o >= accpetShowTime.value;
if (status) {
showAvgDelay.push(i.avg_delay[index]);
}
return status;
});
const {
threshold,
mean,
max,
min,
} = peakShaving.value ? getThreshold(showAvgDelay, 2) : {};
showCreateTime.forEach((o, index) => {
if (dateMap[o]) {
return;
}
const avgDelay = showAvgDelay[index];
if (peakShaving.value) {
if (avgDelay === 0) {
return;
}
if (Math.abs(avgDelay - mean) > threshold && max / min > 2) {
return;
}
}
dateMap[o] = (avgDelay).toFixed(2) * 1;
});
});
let dateList = [];
let valueList = [];
const cateList = [];
Object.keys(cateMap).forEach((i) => {
const {
id,
dateMap,
avgs,
} = cateMap[i];
Object.entries(dateMap).forEach(([key, value]) => {
const time = parseInt(key, 10);
avgs.push([time, value]);
dateList.push(time);
});
const color = getLineColor(id);
if (avgs.length) {
if (!validate.hasOwn(showCates.value, id)) {
showCates.value[id] = true;
}
// 计算平均延迟和成功率
const validAvgs = avgs.filter((a) => a[1] !== 0);
const avg = validAvgs.reduce((a, b) => a + b[1], 0) / validAvgs.length;
const over = avgs.filter((a) => a[1] !== 0).length / avgs.length;
const cateItem = {
id,
name: i,
color,
avg: avg.toFixed(2) * 1,
over: (over * 100).toFixed(2) * 1,
};
if (Number.isNaN(cateItem.avg)) {
cateItem.avg = 0;
}
const titles = [
cateItem.name,
cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
`成功率:${cateItem.over}%`,
];
cateItem.title = titles.filter((s) => s).join('\n');
cateList.push(cateItem);
valueList.push({
id,
name: i,
data: avgs,
itemStyle: {
color,
},
lineStyle: {
color,
},
});
}
});
// 去重
dateList = Array.from(new Set(dateList)).sort((a, b) => a - b);
valueList = valueList.filter((i) => showCates.value[i.id]);
return {
dateList,
cateList,
valueList,
};
});
function switchPeakShaving() {
peakShaving.value = !peakShaving.value;
}
function switchRefresh() {
refreshData.value = !refreshData.value;
}
function toggleMinute(value) {
minute.value = value;
}
function toggleShowCate(id) {
showCates.value[id] = !showCates.value[id];
}
async function loadMonitor() {
await request({
url: (
config.nazhua.nezhaVersion === 'v1' ? config.nazhua.v1ApiMonitorPath : config.nazhua.apiMonitorPath
).replace('{id}', props.info.ID),
}).then((res) => {
const list = config.nazhua.nezhaVersion === 'v1' ? res.data?.data : res.data?.result;
if (Array.isArray(list)) {
monitorData.value = list;
}
}).catch((err) => {
console.error(err);
});
}
let loadMonitorTimer = null;
async function setTimeLoadMonitor(force = false) {
if (loadMonitorTimer) {
clearTimeout(loadMonitorTimer);
}
if (refreshData.value || force) {
await loadMonitor();
}
let monitorRefreshTime = parseInt(config.nazhua.monitorRefreshTime, 10);
// 0 为不刷新
if (monitorRefreshTime === 0) {
return;
}
// 非数字 强制为30
if (Number.isNaN(monitorRefreshTime)) {
monitorRefreshTime = 30;
}
// 最小 10 秒
const sTime = Math.min(monitorRefreshTime, 10);
loadMonitorTimer = setTimeout(() => {
setTimeLoadMonitor();
}, sTime * 1000);
}
onMounted(() => {
setTimeLoadMonitor(true);
});
onUnmounted(() => {
if (loadMonitorTimer) {
clearTimeout(loadMonitorTimer);
}
});
</script>
<style lang="scss" scoped>
.server-monitor-group {
--line-chart-size: 300px;
}
.module-head-group {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
.module-title {
width: max-content;
height: 30px;
line-height: 30px;
font-size: 16px;
color: #eee;
}
.right-box {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.peak-shaving-group,
.refresh-data-group {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
@media screen and (max-width: 1024px) {
cursor: default;
}
.switch-box {
position: relative;
width: 30px;
height: 16px;
background: #999;
border-radius: 10px;
transition: backgroundColor 0.3s;
.switch-dot {
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
transition: left 0.3s;
}
&.active {
background-color: #4caf50;
.switch-dot {
left: 16px;
box-shadow: 1px 1px 2px #000;
}
}
}
.label-text {
color: #ddd;
font-size: 12px;
}
}
.last-update-time-group {
--minute-item-width: 50px;
--minute-item-height: 20px;
display: flex;
align-items: center;
gap: 4px;
.last-update-time-label {
color: #ddd;
height: var(--minute-item-height);
line-height: var(--minute-item-height);
font-size: 12px;
}
@media screen and (max-width: 660px) {
--minute-item-width: 46px;
}
@media screen and (max-width: 600px) {
--minute-item-width: 46px;
}
@media screen and (max-width: 400px) {
.last-update-time-label {
display: none;
}
}
@media screen and (max-width: 330px) {
margin-left: -12px;
}
@media screen and (max-width: 320px) {
margin-left: -18px;
}
}
.minutes {
position: relative;
display: flex;
align-items: center;
// padding: 0 10px;
height: var(--minute-item-height);
background: rgba(#fff, 0.2);
border-radius: calc(var(--minute-item-height) / 2);
.minute-item {
position: relative;
z-index: 10;
width: var(--minute-item-width);
height: var(--minute-item-height);
line-height: var(--minute-item-height);
font-size: 12px;
text-align: center;
cursor: pointer;
color: #aaa;
transition: color 0.3s;
&.active {
color: #fff;
text-shadow: 1px 1px 2px #000;
}
}
.active-arrow {
position: absolute;
top: 0;
left: 0;
width: var(--minute-item-width);
height: var(--minute-item-height);
border-radius: calc(var(--minute-item-height) / 2);
background: #4caf50;
// opacity: 0.5;
transition: left 0.3s;
z-index: 1;
}
}
}
.monitor-cate-group {
--gap-size: 0;
margin: 10px 0;
display: flex;
flex-wrap: wrap;
// justify-content: center;
gap: var(--gap-size);
margin-right: calc(var(--gap-size) * -1);
.monitor-cate-item {
// --cate-item-width: calc(20% - var(--gap-size));
--cate-item-height: 28px;
--cate-item-font-size: 14px;
--cate-color: #fff;
display: flex;
align-items: center;
width: var(--cate-item-width);
height: var(--cate-item-height);
gap: 6px;
padding: 0 6px;
font-size: var(--cate-item-font-size);
// background: rgba(#fff, 0.2);
border-radius: 4px;
cursor: pointer;
.cate-legend {
width: 0.5em;
height: 0.5em;
// border-radius: 50%;
// width: 6px;
// height: calc(var(--cate-item-height) - 10px);
// margin-left: -6px;
background: var(--cate-color);
}
.cate-name {
// flex: 1;
height: var(--cate-item-height);
line-height: calc(var(--cate-item-height) + 2px);
// text-overflow: ellipsis;
// white-space: nowrap;
// overflow: hidden;
color: #eee;
}
.cate-avg-ms {
// width: 55px;
height: var(--cate-item-height);
line-height: calc(var(--cate-item-height) + 2px);
text-align: right;
color: #fff;
}
&.disabled {
filter: grayscale(1);
opacity: 0.5;
}
}
}
</style>