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