Compare commits

...
4 Commits
23 changed files with 265 additions and 149 deletions
+14 -12
View File
@@ -209,7 +209,7 @@ func (cp *commonPage) network(c *gin.Context) {
}))
}
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
authorized := isMember || isViewPasswordVerfied
@@ -217,18 +217,20 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
singleton.SortedServerLock.RLock()
defer singleton.SortedServerLock.RUnlock()
var servers []*model.Server
var serverList []*model.Server
if authorized {
servers = singleton.SortedServerList
serverList = singleton.SortedServerList
} else {
filteredServers := make([]*model.Server, len(singleton.SortedServerListForGuest))
for i, server := range singleton.SortedServerListForGuest {
filteredServer := *server
filteredServer.DDNSDomain = "redacted"
filteredServers[i] = &filteredServer
serverList = singleton.SortedServerListForGuest
}
var servers []*model.Server
for _, server := range serverList {
item := *server
if !withPublicNote {
item.PublicNote = ""
}
servers = filteredServers
servers = append(servers, &item)
}
return utils.Json.Marshal(Data{
@@ -240,7 +242,7 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
}
func (cp *commonPage) home(c *gin.Context) {
stat, err := cp.getServerStat(c)
stat, err := cp.getServerStat(c, true)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
@@ -285,7 +287,7 @@ func (cp *commonPage) ws(c *gin.Context) {
defer conn.Close()
count := 0
for {
stat, err := cp.getServerStat(c)
stat, err := cp.getServerStat(c, false)
if err != nil {
continue
}
+2
View File
@@ -306,6 +306,7 @@ type serverForm struct {
Secret string
Tag string
Note string
PublicNote string
HideForGuest string
EnableDDNS string
EnableIPv4 string
@@ -326,6 +327,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.ID = sf.ID
s.Tag = sf.Tag
s.Note = sf.Note
s.PublicNote = sf.PublicNote
s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on"
s.EnableIPv4 = sf.EnableIPv4 == "on"
+5 -3
View File
@@ -15,7 +15,8 @@ type Server struct {
Name string
Tag string // 分组名
Secret string `gorm:"uniqueIndex" json:"-"`
Note string `json:"-"` // 管理员可见备注
Note string `json:"-"` // 管理员可见备注
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
@@ -54,12 +55,13 @@ func boolToString(b bool) string {
return "false"
}
func (s Server) Marshal() template.JS {
func (s Server) MarshalForDashboard() template.JS {
name, _ := utils.Json.Marshal(s.Name)
tag, _ := utils.Json.Marshal(s.Tag)
note, _ := utils.Json.Marshal(s.Note)
secret, _ := utils.Json.Marshal(s.Secret)
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile)) // #nosec
publicNote, _ := utils.Json.Marshal(s.PublicNote)
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile, publicNote))
}
+1 -1
View File
@@ -18,7 +18,7 @@ func TestServerMarshal(t *testing.T) {
Name: patterns[i],
Tag: patterns[i],
}
serverStr := string(server.Marshal())
serverStr := string(server.MarshalForDashboard())
var serverRestore Server
if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil {
t.Fatalf("Error: %s", serverStr)
+3
View File
@@ -226,6 +226,9 @@ other = "Secret"
[Note]
other = "Note"
[PublicNote]
other = "Public Note"
[LinuxOneKeyInstall]
other = "Linux One-Command Install"
+3
View File
@@ -226,6 +226,9 @@ other = "Secreto"
[Note]
other = "Nota"
[PublicNote]
other = "Nota Pública"
[LinuxOneKeyInstall]
other = "Instalación Linux con Un Solo Clic"
+3
View File
@@ -226,6 +226,9 @@ other = "密钥"
[Note]
other = "备注"
[PublicNote]
other = "公开备注"
[LinuxOneKeyInstall]
other = "Linux 一键安装"
+3
View File
@@ -226,6 +226,9 @@ other = "金鑰"
[Note]
other = "備註"
[PublicNote]
other = "公開備註"
[LinuxOneKeyInstall]
other = "Linux 一鍵安裝"
+1
View File
@@ -331,6 +331,7 @@ function addOrEditServer(server, conf) {
.find("input[name=DisplayIndex]")
.val(server ? server.DisplayIndex : null);
modal.find("textarea[name=Note]").val(server ? server.Note : null);
modal.find("textarea[name=PublicNote]").val(server ? server.PublicNote : null);
if (server) {
modal.find(".secret.field").attr("style", "");
modal.find(".command.field").attr("style", "");
+11
View File
@@ -13,7 +13,18 @@ const mixinsVue = {
this.isMobile = this.checkIsMobile();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
},
mounted() {
this.initDropdown();
},
methods: {
initDropdown() {
if(this.isMobile) $('.ui.dropdown').dropdown({
action: 'hide',
on: 'click',
duration: 100,
direction: 'direction'
});
},
toggleTemplate(template) {
if( template != this.preferredTemplate){
this.preferredTemplate = template;
+5
View File
@@ -381,6 +381,7 @@ td.ping-network-quality {
}
.network-box .chartTitle {
cursor: pointer;
text-align: center;
font-size: 18px;
margin: 18px 0px 15px 0px;
@@ -483,6 +484,10 @@ footer p{
min-height: calc(100vh - 90px);
min-height: calc(var(--vh, 1vh) * 100 - 90px);
}
#chartbox {
min-height: calc(100vh - 170px);
min-height: calc(var(--vh, 1vh) * 100 - 170px);
}
.content {
padding: 0;
margin-bottom: 10px;
+1 -1
View File
@@ -10,7 +10,7 @@
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="/static/main.js?v20240714"></script>
<script src="/static/main.js?v2024927"></script>
<script>
(function () {
updateLang({{.LANG }});
+4
View File
@@ -56,6 +56,10 @@
<label>{{tr "Note"}}</label>
<textarea name="Note"></textarea>
</div>
<div class="field">
<label>{{tr "PublicNote"}}</label>
<textarea name="PublicNote"></textarea>
</div>
<div class="command field">
<label>{{tr "LinuxOneKeyInstall"}}</label>
<div class="ui message">
+3 -1
View File
@@ -36,6 +36,7 @@
<th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th>
<th>{{tr "PublicNote"}}</th>
<th>{{tr "Administration"}}</th>
</tr>
</thead>
@@ -76,12 +77,13 @@
</button>
</td>
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.Note}}</td>
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.PublicNote}}</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="connectToServer({{$server.ID}})">
<i class="terminal icon"></i>
</button>
<button class="ui button" onclick="addOrEditServer({{$server.Marshal}})">
<button class="ui button" onclick="addOrEditServer({{$server.MarshalForDashboard}})">
<i class="edit icon"></i>
</button>
<button class="ui button"
+1 -1
View File
@@ -30,7 +30,7 @@
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
<script src="/static/theme-default/js/mixin.js?v20240911"></script>
<script>
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
</script>
+2 -2
View File
@@ -7,7 +7,7 @@
</a>
<a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
<template v-if="isMobile">
<div class="item ui simple dropdown">
<div class="item ui dropdown" :class="{ simple: !isMobile }">
<div class="text"><i class="bi bi-gear-wide-connected icon" style="margin-right:3px;"></i>{{tr "Feature" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a href="/service" class="item"><i class="rss icon"></i>{{tr "Services" }}</a>
@@ -20,7 +20,7 @@
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
</template>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<div class="item ui simple dropdown">
<div class="item ui dropdown" :class="{ simple: !isMobile }">
<div class="text"><i class="bi bi-incognito icon" style="margin-right:3px;"></i>{{tr "Template" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
+1 -1
View File
@@ -18,7 +18,7 @@
</div>
</div>
<div class="ui container">
<div ref="chartDom" style="margin-top: 15px;height: 520px;overflow: hidden"></div>
<div ref="chartDom" style="margin-top: 15px;height: auto;overflow: hidden"></div>
</div>
</div>
+1 -1
View File
@@ -26,7 +26,7 @@
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240907">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240909">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
<script src="/static/theme-server-status/js/mixin.js?v20240907"></script>
@@ -18,7 +18,7 @@
<tbody id="servers">
<template v-for="(node,index) in nodesNoTag">
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle" :class="index % 2 === 0 ? 'odd': 'even'"
aria-expanded="false" @click="showCharts($event, node.ID)">
aria-expanded="false" @click="showCharts(node.ID)">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
@@ -143,7 +143,7 @@
@#node.host.Version#@
</span>
<span class="node-echarts-expand">
<div class="chartbox" chartbox-show="0" :key="node.ID" :ref="`chart${node.ID}`" style="width: 100%; height: auto;"></div>
<div class="chartbox" :id="`chart-${node.ID}`" chartbox-show="0" :key="node.ID" style="width: 100%; height: auto;"></div>
</span>
</div>
</div>
+2 -2
View File
@@ -21,7 +21,7 @@
<tbody id="servers">
<template v-for="(node,index) in group.data">
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
:class="index % 2 === 0 ? 'odd': 'even'" aria-expanded="false" @click="showCharts($event, node.ID)">
:class="index % 2 === 0 ? 'odd': 'even'" aria-expanded="false" @click="showCharts(node.ID)">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
@@ -146,7 +146,7 @@
@#node.host.Version#@
</span>
<span class="node-echarts-expand">
<div class="chartbox" chartbox-show="0" :key="node.ID" :ref="`chart${node.ID}`" style="width: 100%; height: auto;"></div>
<div class="chartbox" :id="`chart-${node.ID}`" chartbox-show="0" :key="node.ID" style="width: 100%; height: auto;"></div>
</span>
</div>
</div>
+142 -87
View File
@@ -483,24 +483,27 @@
// 如果所有元素的 Temperature 都为 0,则返回一个默认值 0
return 0;
},
showCharts(event, id) {
const chartContainer = this.$refs[`chart${id}`][0];
const chartboxShow = chartContainer.getAttribute('chartbox-show');
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
const isAriaExpandedFalse = event.currentTarget.getAttribute('aria-expanded') === 'false';
if (!isAriaExpandedFalse) return;
showCharts(id,changeChartboxShow=true) {
const chartContainer = document.getElementById(`chart-${id}`);
if(changeChartboxShow){
const chartboxShow = chartContainer.getAttribute('chartbox-show');
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
const collapseContainer = document.getElementById(`r${id}`);
const isAriaExpandedFalse = collapseContainer.getAttribute('aria-expanded') === 'false';
if (!isAriaExpandedFalse) return;
}
// 发起数据请求
const url = `/api/v1/monitor/${id}`;
fetch(url)
.then(response => response.json())
.then(data => {
if (data.result) { // 数据请求成功,更新数据并渲染图表
this.chartDataList[id - 1] = data.result;
this.chartDataList[id] = data.result;
this.$nextTick(() => {
this.renderCharts(id);
});
} else {
console.log('this agent (id:'+ id + ') has no monitor.');
console.log('this server (id:'+ id + ') has no monitor.');
}
})
.catch(error => {
@@ -508,22 +511,18 @@
});
},
renderCharts(id, reload = false) {
if (!this.chartDataList[id - 1]) return;
const chartData = this.chartDataList[id - 1];
const chartContainer = this.$refs[`chart${id}`][0];
if (reload) { //点击切换亮色/暗色风格模式时,重新载入echarts图表的逻辑,
// 第一步,查找已经渲染出的图表容器,并销毁它
if (!this.chartDataList[id]) return;
const chartData = this.chartDataList[id];
const chartContainer = document.getElementById(`chart-${id}`);
if (reload) {
const existingChart = echarts.getInstanceByDom(chartContainer);
if (existingChart) existingChart.dispose();
// 第二步,如果图表容器处于不可见状态chartboxShow=0,不重新渲染出新的图表,
// 如果图表容器处于可见状态chartboxShow=1,重新渲染出新的图表
const chartboxShow = chartContainer.getAttribute('chartbox-show');
if ( chartboxShow === '0' ) return;
}
// 定义图表参数值
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
const fontSize = this.isMobile ? 10 : 14;
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 30 : 40) : (this.isMobile ? 25 : 36);
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 25 : 36);
const gridRight = this.isMobile ? 5 : 20;
const legendLeft = this.isMobile ? 'center' : 'center';
const legendTop = this.isMobile ? 5 : 5;
@@ -537,47 +536,91 @@
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 chart = echarts.init(chartContainer, chartTheme, {
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
const chart = echarts.init(chartContainer, chartTheme, { // init图表
renderer: 'canvas',
useDirtyRect: false,
width: 'auto',
height: 300,
height: 300
});
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
const seriesData = chartData.map(item => {
let legendData = [];
let seriesData = [];
chartData.forEach((item,key)=> {
let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => {
if(avgDelay > 0 && avgDelay < MaxTCPPingValue){
loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
return [item.created_at[index], avgDelay.toFixed(2)];
}else{
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]
);
}
});
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
return {
name: item.monitor_name,
type: 'line',
smooth: true,
symbol: 'none',
data: data,
connectNulls: true,
legendHoverLink: false,
emphasis: {
disabled: true
},
lineStyle: {
width: lineStyleWidth
}
};
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
}
}
}
);
});
const legendData = chartData.map(item => item.monitor_name);
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 autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
const height = 300 + autoIncrement;
const gridTop = 40 + autoIncrement;
const legendIcon = this.isMobile ? 'rect' : "";
@@ -587,39 +630,24 @@
width: 'auto',
height: height
});
const option = {
color: this.colors,
backgroundColor: backgroundColor,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: gridTop,
left: gridLeft,
right: gridRight,
},
title: {
show: false
},
tooltip: {
trigger: 'axis',
backgroundColor: tooltipBackgroundColor,
borderColor: tooltipBorderColor,
textStyle: {
fontSize: fontSize,
color: fontColor
}
},
legend: {
data: legendData,
show: true,
icon: legendIcon,
textStyle: {
fontSize: fontSize,
color: fontColor
},
top: legendTop,
bottom: 0,
left: legendLeft,
padding: legendPadding,
itemWidth: itemWidth,
itemHeight: itemHeight,
show: false,
},
series: seriesData.flat(),
xAxis: {
type: 'time',
data: xAxisData,
axisLabel: {
textStyle: {
fontSize: fontSize
@@ -639,30 +667,57 @@
}
}
},
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
}
],
series: seriesData,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: gridTop,
left: gridLeft,
right: gridRight
}
]
};
chart.setOption(option);
},
reloadCharts() { // 重新加载所有图表
reloadCharts() {
this.servers.forEach(node => {
const id = node.ID;
const chartData = this.chartDataList[id - 1];
const chartData = this.chartDataList[id];
if (chartData) {
this.renderCharts(id,true);
}
+36 -33
View File
@@ -15,7 +15,7 @@
</li>
</ul>
</div>
<div class="chartTitle"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</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" .}}
@@ -32,7 +32,8 @@
chartTitle: '',
chartCountryCode: '',
chart: null,
currentServerId: ''
currentServerId: '',
nextServerId: '',
},
mixins: [mixinsVue],
created() {
@@ -66,13 +67,13 @@
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 - 200) : (window.innerHeight - 250);
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;
@@ -86,18 +87,27 @@
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 ? 30 : 42;
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 0.9;
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)=> {
@@ -112,9 +122,6 @@
loss += 1;
const lossrate = 100 * loss / (index + 1);
if(lossrate != 100) {
data['main'].push(
[item.created_at[index], MaxTCPPingValue, lossrate]
);
data['markLine'].push({
xAxis: item.created_at[index],
label: { show: false },
@@ -143,11 +150,9 @@
);
}
});
// 处理legendData
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
legendName = `${item.monitor_name} ${totalLossRate}%`;
legendData.push(legendName);
// 处理seriesData
seriesData.push(
{
name: legendName,
@@ -228,29 +233,22 @@
});
// 设置图表配置项
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(),
// X轴设置
xAxis: {
type: 'time',
axisLabel: {
@@ -259,7 +257,6 @@
}
}
},
// Y轴设置
yAxis: {
type: 'value',
axisLabel: {
@@ -273,7 +270,6 @@
}
}
},
// 图例设置
legend: {
data: legendData,
show: true,
@@ -289,7 +285,6 @@
itemWidth: itemWidth,
itemHeight: itemHeight,
},
// 提示框设置
tooltip: {
trigger: 'axis',
backgroundColor: tooltipBackgroundColor,
@@ -312,7 +307,6 @@
return tooltipContent;
}
},
// 数据缩放设置
dataZoom: [
{
type: 'slider',
@@ -321,17 +315,16 @@
}
]
};
// 设置图表的配置选项
this.chart.setOption(option);
setTimeout(() => {
this.chart.hideLoading();
this.chart.setOption(option);
}, 1000);
},
reloadCharts() { // 重新加载所有图表
this.servers.forEach(node => {
const id = node.ID;
const chartData = this.chartDataList[id];
if (chartData) {
this.renderCharts(id,true);
}
});
reloadCharts() {
const chartData = this.chartDataList[this.currentServerId];
if (chartData) {
this.renderCharts(this.currentServerId,true);
}
},
disposeCharts(chart){
chart.dispose();
@@ -341,6 +334,16 @@
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();
+19 -2
View File
@@ -3,6 +3,7 @@ package rpc
import (
"errors"
"io"
"sync"
"sync/atomic"
"time"
)
@@ -14,6 +15,18 @@ type ioStreamContext struct {
agentIoConnectCh chan struct{}
}
type bp struct {
buf []byte
}
var bufPool = sync.Pool{
New: func() any {
return &bp{
buf: make([]byte, 1024*1024),
}
},
}
func (s *NezhaHandler) CreateStream(streamId string) {
s.ioStreamMutex.Lock()
defer s.ioStreamMutex.Unlock()
@@ -117,7 +130,9 @@ LOOP:
endCh := make(chan struct{})
go func() {
_, innerErr := io.CopyBuffer(stream.userIo, stream.agentIo, make([]byte, 1048576))
bp := bufPool.Get().(*bp)
defer bufPool.Put(bp)
_, innerErr := io.CopyBuffer(stream.userIo, stream.agentIo, bp.buf)
if innerErr != nil {
err = innerErr
}
@@ -126,7 +141,9 @@ LOOP:
}
}()
go func() {
_, innerErr := io.CopyBuffer(stream.agentIo, stream.userIo, make([]byte, 1048576))
bp := bufPool.Get().(*bp)
defer bufPool.Put(bp)
_, innerErr := io.CopyBuffer(stream.agentIo, stream.userIo, bp.buf)
if innerErr != nil {
err = innerErr
}