mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-23 05:08:13 -05:00
364 lines
18 KiB
HTML
Vendored
364 lines
18 KiB
HTML
Vendored
{{define "theme-server-status/network"}}
|
||
{{template "theme-server-status/header" .}}
|
||
{{template "theme-server-status/menu" .}}
|
||
<div class="container-fluid content network-box">
|
||
<div class="network-box-header btn-group">
|
||
<div class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||
<i class="bi bi-list-ul"></i> {{tr "NetworkSpiterList"}} <i class="bi bi-chevron-compact-down"></i>
|
||
</div>
|
||
<ul class="dropdown-menu">
|
||
<li class="input-group fixed-top">
|
||
<input type="text" id="dropdown-search" class="form-control" placeholder="Search...">
|
||
</li>
|
||
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)">
|
||
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'rb')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div class="chartTitle" @click="showCharts(nextServerId)"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
|
||
<div id="chartbox" style="width:100%;height:auto;"></div>
|
||
</div>
|
||
{{template "theme-server-status/footer" .}}
|
||
<script>
|
||
new Vue({
|
||
el: '#app',
|
||
delimiters: ['@#', '#@'],
|
||
data: {
|
||
page: 'network',
|
||
defaultTemplate: "{{.Conf.Site.Theme}}",
|
||
templates: "{{.Themes }}",
|
||
servers: [],
|
||
chartDataList: [],
|
||
chartTitle: '',
|
||
chartCountryCode: '',
|
||
chart: null,
|
||
currentServerId: '',
|
||
nextServerId: '',
|
||
},
|
||
mixins: [mixinsVue],
|
||
created() {
|
||
this.servers = JSON.parse('{{.Servers}}').servers;
|
||
this.showCharts(this.servers[0].ID);
|
||
},
|
||
mounted() {
|
||
this.initSearch();
|
||
},
|
||
methods: {
|
||
showCharts(id) {
|
||
// 发起数据请求
|
||
const url = `/api/v1/monitor/${id}`;
|
||
fetch(url)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.result) { // 数据请求成功,更新数据并渲染图表
|
||
this.chartDataList[id] = data.result;
|
||
this.$nextTick(() => {
|
||
this.renderCharts(id);
|
||
});
|
||
} else {
|
||
console.log('this server (id:'+ id + ') has no monitor.');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching data:', error);
|
||
});
|
||
},
|
||
renderCharts(id, reload = false) {
|
||
if(!this.chartDataList[id]) return;
|
||
if(this.chart) this.disposeCharts(this.chart);
|
||
this.currentServerId = id;
|
||
this.nextServerId = this.getNextServerId(id);
|
||
this.chartCountryCode = this.getServerCountryCode(id);
|
||
this.chartTitle = this.chartDataList[id][0].server_name;
|
||
const chartData = this.chartDataList[id];
|
||
const chartContainer = document.getElementById('chartbox');
|
||
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
||
const autoheight = this.isMobile ? (window.innerHeight - 180) : (window.innerHeight - 250);
|
||
const fontSize = this.isMobile ? 10 : 14;
|
||
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 30 : 36);
|
||
const gridRight = this.isMobile ? 12 : 20;
|
||
const legendLeft = this.isMobile ? 'center' : 'center';
|
||
const legendTop = this.isMobile ? 5 : 5;
|
||
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
||
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
|
||
const chartTheme = this.theme == "dark" ? "dark" : "default";
|
||
const backgroundColor = this.theme == "dark" ? '' : '';
|
||
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
|
||
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)");
|
||
const lineStyleWidth = this.isMobile ? 1 : 2;
|
||
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
||
const markPointSymbolSize = this.isMobile ? 36 : 42;
|
||
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 1;
|
||
const markPointFontSize = this.isMobile ? 8 : 10;
|
||
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
||
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
|
||
const showLoadingMaskColor = this.theme == "dark" ? 'rgba(0, 0, 0, 0)' : 'rgba(255, 255, 255, 0)';
|
||
const showLoadingTextColor = this.theme == "dark" ? 'rgba(241, 241, 241, 1)' : 'rgba(0, 0, 0, 1)';
|
||
const showLoadingColor = this.theme == "dark" ? '#D2B206' : '#FFDF32';
|
||
this.chart = echarts.init(chartContainer, chartTheme, { // init图表
|
||
renderer: 'canvas',
|
||
useDirtyRect: false,
|
||
width: 'auto',
|
||
height: autoheight,
|
||
});
|
||
this.chart.showLoading({
|
||
text: 'loading',
|
||
textColor: showLoadingTextColor,
|
||
color: showLoadingColor,
|
||
maskColor: showLoadingMaskColor,
|
||
zlevel: 2
|
||
});
|
||
let legendData = [];
|
||
let seriesData = [];
|
||
chartData.forEach((item,key)=> {
|
||
let loss = 0;
|
||
let totalLossRate = 0;
|
||
let legendName = '';
|
||
let data = { main: [], markLine: []};
|
||
item.avg_delay.forEach((avgDelay, index) => {
|
||
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
|
||
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
|
||
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
|
||
loss += 1;
|
||
const lossrate = 100 * loss / (index + 1);
|
||
if(lossrate != 100) {
|
||
data['markLine'].push({
|
||
xAxis: item.created_at[index],
|
||
label: { show: false },
|
||
emphasis: { disabled: true },
|
||
lineStyle: { type: "solid" }
|
||
});
|
||
}
|
||
} else if (avgDelay >= threshold && avgDelay < MaxTCPPingValue){ // 相对丢包
|
||
loss += 1;
|
||
const lossrate = 100 * loss / (index + 1);
|
||
if(lossrate != 100) {
|
||
data['main'].push(
|
||
[item.created_at[index], avgDelay, lossrate]
|
||
);
|
||
data['markLine'].push({
|
||
xAxis: item.created_at[index],
|
||
label: { show: false },
|
||
emphasis: { disabled: true },
|
||
lineStyle: { type: "solid" }
|
||
});
|
||
}
|
||
} else { // 未丢包
|
||
const lossrate = 100 * loss / (index + 1);
|
||
data['main'].push(
|
||
[item.created_at[index], avgDelay, lossrate]
|
||
);
|
||
}
|
||
});
|
||
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
||
legendName = `${item.monitor_name} ${totalLossRate}%`;
|
||
legendData.push(legendName);
|
||
seriesData.push(
|
||
{
|
||
name: legendName,
|
||
type: 'line',
|
||
smooth: true,
|
||
symbol: 'none',
|
||
connectNulls: true,
|
||
legendHoverLink: false,
|
||
emphasis: {
|
||
disabled: true
|
||
},
|
||
lineStyle: {
|
||
width: lineStyleWidth
|
||
},
|
||
data: data['main'],
|
||
markLine: {
|
||
symbol: "none",
|
||
symbolSize :0,
|
||
data: data['markLine'],
|
||
itemStyle: {
|
||
opacity: markLineItemStyleOpacity
|
||
},
|
||
lineStyle:{
|
||
width: markLineLineStyleWidth
|
||
}
|
||
},
|
||
markPoint: {
|
||
data: [
|
||
{
|
||
name: 'Max',
|
||
type: 'max',
|
||
symbol: 'pin',
|
||
itemStyle: {
|
||
opacity: markPointItemStyleOpacity
|
||
},
|
||
symbolSize: markPointSymbolSize,
|
||
label: {
|
||
fontSize: markPointFontSize,
|
||
formatter: function (params) {
|
||
return Math.round(params.value);
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: 'Min',
|
||
type: 'min',
|
||
symbol: 'pin',
|
||
itemStyle: {
|
||
opacity: markPointItemStyleOpacity
|
||
},
|
||
symbolSize: markPointSymbolSize,
|
||
label: {
|
||
fontSize: markPointFontSize,
|
||
offset: [0, 8],
|
||
formatter: function (params) {
|
||
return Math.round(params.value);
|
||
}
|
||
},
|
||
symbolRotate: 180
|
||
}
|
||
]
|
||
}
|
||
}
|
||
);
|
||
});
|
||
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
|
||
const height = autoheight + autoIncrement;
|
||
const gridTop = this.isMobile ? ( 60 + autoIncrement) : (80 + autoIncrement);
|
||
const gridBottom = this.isMobile ? 70 : 90;
|
||
const legendIcon = this.isMobile ? 'rect' : "";
|
||
const itemWidth = this.isMobile ? 10 : 25;
|
||
const itemHeight = this.isMobile ? 10 : 14;
|
||
this.chart.resize({
|
||
width: 'auto',
|
||
height: height
|
||
});
|
||
// 设置图表配置项
|
||
const option = {
|
||
color: this.colors,
|
||
backgroundColor: backgroundColor,
|
||
textStyle: {
|
||
fontSize: fontSize,
|
||
color: fontColor
|
||
},
|
||
grid: {
|
||
top: gridTop,
|
||
left: gridLeft,
|
||
right: gridRight,
|
||
bottom: gridBottom
|
||
},
|
||
title: {
|
||
show: false,
|
||
},
|
||
series: seriesData.flat(),
|
||
xAxis: {
|
||
type: 'time',
|
||
axisLabel: {
|
||
textStyle: {
|
||
fontSize: fontSize
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
textStyle: {
|
||
fontSize: fontSize
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
width: splitLineWidth
|
||
}
|
||
}
|
||
},
|
||
legend: {
|
||
data: legendData,
|
||
show: true,
|
||
icon: legendIcon,
|
||
textStyle: {
|
||
fontSize: fontSize,
|
||
color: fontColor
|
||
},
|
||
top: legendTop,
|
||
bottom: 0,
|
||
left: legendLeft,
|
||
padding: legendPadding,
|
||
itemWidth: itemWidth,
|
||
itemHeight: itemHeight,
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: tooltipBackgroundColor,
|
||
borderColor: tooltipBorderColor,
|
||
textStyle: {
|
||
fontSize: fontSize,
|
||
color: fontColor
|
||
},
|
||
formatter: function (params) {
|
||
let tooltipContent = '';
|
||
const formattedTime = new Date(params[0].value[0]).toLocaleString();
|
||
tooltipContent += `<span style="line-height:2em">${formattedTime}</span><br>`;
|
||
params.forEach(param => {
|
||
const formattedTime = new Date(param.value[0]).toLocaleString();
|
||
if (!param.seriesName.includes('stack')) {
|
||
const name = param.seriesName.replace(/\s\d+(\.\d+)?%$/, '');
|
||
tooltipContent += `<span style="line-height:2em">${param.marker} ${name} ${param.value[2].toFixed(1)}% ${param.value[1].toFixed(2)}</span><br>`;
|
||
}
|
||
});
|
||
return tooltipContent;
|
||
}
|
||
},
|
||
dataZoom: [
|
||
{
|
||
type: 'slider',
|
||
start: 0,
|
||
end: 100
|
||
}
|
||
]
|
||
};
|
||
setTimeout(() => {
|
||
this.chart.hideLoading();
|
||
this.chart.setOption(option);
|
||
}, 1000);
|
||
},
|
||
reloadCharts() {
|
||
const chartData = this.chartDataList[this.currentServerId];
|
||
if (chartData) {
|
||
this.renderCharts(this.currentServerId,true);
|
||
}
|
||
},
|
||
disposeCharts(chart){
|
||
chart.dispose();
|
||
chart = null;
|
||
},
|
||
getServerCountryCode(id){
|
||
const result = this.servers.find(item => item.ID == id);
|
||
return result.Host.CountryCode ? result.Host.CountryCode : 'rb';
|
||
},
|
||
getNextServerId(id) {
|
||
const currentIndex = this.servers.findIndex(item => item.ID === id);
|
||
if (currentIndex === -1) {
|
||
return this.servers[0].ID;
|
||
}
|
||
// 判断是否有下一个元素
|
||
const nextIndex = currentIndex + 1;
|
||
// 如果有下一个元素,返回下一个元素的 ID;否则返回第一个元素的 ID
|
||
return nextIndex < this.servers.length ? this.servers[nextIndex].ID : this.servers[0].ID;
|
||
},
|
||
initSearch() {
|
||
$('#dropdown-search').on('keyup', function() {
|
||
var searchTerm = $(this).val().toLowerCase();
|
||
$('.dropdown-menu .dropdown-item').each(function() {
|
||
var text = $(this).text().toLowerCase();
|
||
if (text.indexOf(searchTerm) > -1) {
|
||
$(this).removeClass('hidden').addClass('visible'); // 显示元素
|
||
} else {
|
||
$(this).removeClass('visible').addClass('hidden'); // 隐藏元素
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
{{end}}
|