Compare commits

...
7 Commits
Author SHA1 Message Date
naiba 2b40040ec5 chore: ignore gosec errors 2024-10-02 10:10:06 +08:00
nap0o 60ada33ee9 feat: status-server主题增加agent账单信息展示 (#424)
1. 首页通过在后台配置PublicNote字段,实现agent账单信息展示
2. 优化顶部导航,增加当前位置标识
3. 优化network页移动端样式
4. 优化后台server页展示,隐藏掉一些不重要的信息
5. 一些其他小优化
使用说明:https://github.com/naiba/nezha/pull/424#issuecomment-2386837658
2024-10-02 10:05:53 +08:00
naiba 7940e010e0 Refactor file compression logic in release workflow 2024-09-30 20:49:51 +08:00
naiba 2df57e6249 🐛 修复展示对游客隐藏服务器 2024-09-28 01:19:02 +08:00
naiba 106d58575b 增加透传给前端的自定义字段方便前端扩展功能 2024-09-28 00:16:54 +08:00
UUBulb bdf36276da improve: use sync.Pool for buffer allocation (#423) 2024-09-27 18:50:19 +08:00
nap0o f32f127dfc improve: status-server主题network页 (#422) 2024-09-11 21:50:40 +08:00
29 changed files with 783 additions and 183 deletions
+2 -1
View File
@@ -75,7 +75,8 @@ jobs:
for file in assets/*/*/*; do for file in assets/*/*/*; do
if [ -f "$file" ]; then if [ -f "$file" ]; then
chmod +x "$file" chmod +x "$file"
zip -jr "$file.zip" "$file" export fileWithoutExt=${file%.*}
zip -jr "$fileWithoutExt.zip" "$file"
fi fi
done done
+1 -1
View File
@@ -42,4 +42,4 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
uses: securego/gosec@master uses: securego/gosec@master
with: with:
args: --exclude=G104,G402 ./... args: --exclude=G104,G402,G115,G203 ./...
+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) _, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified) _, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
authorized := isMember || isViewPasswordVerfied authorized := isMember || isViewPasswordVerfied
@@ -217,18 +217,20 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
singleton.SortedServerLock.RLock() singleton.SortedServerLock.RLock()
defer singleton.SortedServerLock.RUnlock() defer singleton.SortedServerLock.RUnlock()
var servers []*model.Server var serverList []*model.Server
if authorized { if authorized {
servers = singleton.SortedServerList serverList = singleton.SortedServerList
} else { } else {
filteredServers := make([]*model.Server, len(singleton.SortedServerListForGuest)) serverList = singleton.SortedServerListForGuest
for i, server := range singleton.SortedServerListForGuest { }
filteredServer := *server
filteredServer.DDNSDomain = "redacted" var servers []*model.Server
filteredServers[i] = &filteredServer for _, server := range serverList {
item := *server
if !withPublicNote {
item.PublicNote = ""
} }
servers = filteredServers servers = append(servers, &item)
} }
return utils.Json.Marshal(Data{ 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) { func (cp *commonPage) home(c *gin.Context) {
stat, err := cp.getServerStat(c) stat, err := cp.getServerStat(c, true)
if err != nil { if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError, Code: http.StatusInternalServerError,
@@ -285,7 +287,7 @@ func (cp *commonPage) ws(c *gin.Context) {
defer conn.Close() defer conn.Close()
count := 0 count := 0
for { for {
stat, err := cp.getServerStat(c) stat, err := cp.getServerStat(c, false)
if err != nil { if err != nil {
continue continue
} }
+2
View File
@@ -306,6 +306,7 @@ type serverForm struct {
Secret string Secret string
Tag string Tag string
Note string Note string
PublicNote string
HideForGuest string HideForGuest string
EnableDDNS string EnableDDNS string
EnableIPv4 string EnableIPv4 string
@@ -326,6 +327,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.ID = sf.ID s.ID = sf.ID
s.Tag = sf.Tag s.Tag = sf.Tag
s.Note = sf.Note s.Note = sf.Note
s.PublicNote = sf.PublicNote
s.HideForGuest = sf.HideForGuest == "on" s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on" s.EnableDDNS = sf.EnableDDNS == "on"
s.EnableIPv4 = sf.EnableIPv4 == "on" s.EnableIPv4 = sf.EnableIPv4 == "on"
+5 -3
View File
@@ -15,7 +15,8 @@ type Server struct {
Name string Name string
Tag string // 分组名 Tag string // 分组名
Secret string `gorm:"uniqueIndex" json:"-"` Secret string `gorm:"uniqueIndex" json:"-"`
Note string `json:"-"` // 管理员可见备注 Note string `json:"-"` // 管理员可见备注
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
DisplayIndex int // 展示排序,越大越靠前 DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏 HideForGuest bool // 对游客隐藏
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效 EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
@@ -54,12 +55,13 @@ func boolToString(b bool) string {
return "false" return "false"
} }
func (s Server) Marshal() template.JS { func (s Server) MarshalForDashboard() template.JS {
name, _ := utils.Json.Marshal(s.Name) name, _ := utils.Json.Marshal(s.Name)
tag, _ := utils.Json.Marshal(s.Tag) tag, _ := utils.Json.Marshal(s.Tag)
note, _ := utils.Json.Marshal(s.Note) note, _ := utils.Json.Marshal(s.Note)
secret, _ := utils.Json.Marshal(s.Secret) secret, _ := utils.Json.Marshal(s.Secret)
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain) ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile) 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], Name: patterns[i],
Tag: patterns[i], Tag: patterns[i],
} }
serverStr := string(server.Marshal()) serverStr := string(server.MarshalForDashboard())
var serverRestore Server var serverRestore Server
if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil { if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil {
t.Fatalf("Error: %s", serverStr) t.Fatalf("Error: %s", serverStr)
+18
View File
@@ -226,6 +226,9 @@ other = "Secret"
[Note] [Note]
other = "Note" other = "Note"
[PublicNote]
other = "Public Note"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux One-Command Install" other = "Linux One-Command Install"
@@ -687,3 +690,18 @@ other = "Cancel"
[FMError] [FMError]
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again." other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
[Remaining]
other = "Remaining"
[Lifetime]
other = "Lifetime"
[Price]
other = "Price"
[Expired]
other = "Expired"
[Days]
other = "d"
+18
View File
@@ -226,6 +226,9 @@ other = "Secreto"
[Note] [Note]
other = "Nota" other = "Nota"
[PublicNote]
other = "Nota Pública"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Instalación Linux con Un Solo Clic" other = "Instalación Linux con Un Solo Clic"
@@ -687,3 +690,18 @@ other = "Cancelar"
[FMError] [FMError]
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM." other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
[Remaining]
other = "Remaining"
[Lifetime]
other = "Lifetime"
[Price]
other = "Price"
[Expired]
other = "Expired"
[Days]
other = "d"
+18
View File
@@ -226,6 +226,9 @@ other = "密钥"
[Note] [Note]
other = "备注" other = "备注"
[PublicNote]
other = "公开备注"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux 一键安装" other = "Linux 一键安装"
@@ -687,3 +690,18 @@ other = "取消"
[FMError] [FMError]
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。" other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
[Remaining]
other = "剩余"
[Lifetime]
other = "永续"
[Price]
other = "价格"
[Expired]
other = "已到期"
[Days]
other = "天"
+18
View File
@@ -226,6 +226,9 @@ other = "金鑰"
[Note] [Note]
other = "備註" other = "備註"
[PublicNote]
other = "公開備註"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux 一鍵安裝" other = "Linux 一鍵安裝"
@@ -687,3 +690,18 @@ other = "取消"
[FMError] [FMError]
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。" other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
[Remaining]
other = "剩餘"
[Lifetime]
other = "永續"
[Price]
other = "價格"
[Expired]
other = "已到期"
[Days]
other = "天"
+1
View File
@@ -331,6 +331,7 @@ function addOrEditServer(server, conf) {
.find("input[name=DisplayIndex]") .find("input[name=DisplayIndex]")
.val(server ? server.DisplayIndex : null); .val(server ? server.DisplayIndex : null);
modal.find("textarea[name=Note]").val(server ? server.Note : null); modal.find("textarea[name=Note]").val(server ? server.Note : null);
modal.find("textarea[name=PublicNote]").val(server ? server.PublicNote : null);
if (server) { if (server) {
modal.find(".secret.field").attr("style", ""); modal.find(".secret.field").attr("style", "");
modal.find(".command.field").attr("style", ""); modal.find(".command.field").attr("style", "");
+11
View File
@@ -13,7 +13,18 @@ const mixinsVue = {
this.isMobile = this.checkIsMobile(); this.isMobile = this.checkIsMobile();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate; this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
}, },
mounted() {
this.initDropdown();
},
methods: { methods: {
initDropdown() {
if(this.isMobile) $('.ui.dropdown').dropdown({
action: 'hide',
on: 'click',
duration: 100,
direction: 'direction'
});
},
toggleTemplate(template) { toggleTemplate(template) {
if( template != this.preferredTemplate){ if( template != this.preferredTemplate){
this.preferredTemplate = template; this.preferredTemplate = template;
+8 -3
View File
@@ -8,6 +8,11 @@ body[theme="dark"] .navbar .navbar-brand {
color: #ffffff; color: #ffffff;
} }
body[theme="dark"] .navbar .navbar-nav li.pc-active a,
body[theme="dark"] .navbar .navbar-nav li.m-active a{
color: rgba(73, 146, 255, 1);
}
body[theme="dark"] .navbar .dropdown-menu { body[theme="dark"] .navbar .dropdown-menu {
background-color: rgba(23, 26, 30, 1); background-color: rgba(23, 26, 30, 1);
border-color: rgba(49, 54, 59, 1); border-color: rgba(49, 54, 59, 1);
@@ -88,8 +93,8 @@ body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
/* expandRow展开部分样式结束 */ /* expandRow展开部分样式结束 */
body[theme="dark"] .progress { body[theme="dark"] .progress {
background-image: linear-gradient(#2c2c2c 0,rgba(28, 29, 38, 1) 100%); background-image: none;
background-color: rgba(28, 29, 38, 1); background-color: rgba(255, 255, 255, 0.075);
} }
body[theme="dark"] .progress-bar { body[theme="dark"] .progress-bar {
@@ -174,5 +179,5 @@ body[theme="dark"] .toolbox i{
} }
body[theme="dark"] .network-box .network-box-header{ body[theme="dark"] .network-box .network-box-header{
border-bottom: 1px solid rgba(110, 112, 121, 0.25); border-bottom-color: rgba(110, 112, 121, 0.25);
} }
+7 -2
View File
@@ -16,10 +16,15 @@ body[theme="light"] .navbar .navbar-brand {
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
body[theme="light"] .navbar .navbar-nav > li > a{ body[theme="light"] .navbar .navbar-nav > li > a {
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
body[theme="light"] .navbar .navbar-nav li.pc-active a,
body[theme="light"] .navbar .navbar-nav li.m-active a {
color: rgba(73, 146, 255, 1);
}
body[theme="light"] .navbar .navbar-nav > .open > a:focus, body[theme="light"] .navbar .navbar-nav > .open > a:focus,
body[theme="light"] .navbar .navbar-nav > .open > a:hover, body[theme="light"] .navbar .navbar-nav > .open > a:hover,
body[theme="light"] .navbar .navbar-nav > .active > a, body[theme="light"] .navbar .navbar-nav > .active > a,
@@ -198,7 +203,7 @@ body[theme="light"] .modal-header i.xclose{
} }
body[theme="light"] .network-box .network-box-header{ body[theme="light"] .network-box .network-box-header{
border-bottom: 1px solid rgba(224, 230, 241, 0.6); border-bottom-color: rgba(224, 230, 241, 0.6);
} }
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
+32 -2
View File
@@ -237,8 +237,8 @@ tr.accordion-toggle{
} }
.node-cell.network { .node-cell.network {
min-width: 100px; min-width: 110px;
max-width: 100px; max-width: 110px;
} }
.node-cell.traffic { .node-cell.traffic {
@@ -250,6 +250,24 @@ tr.accordion-toggle{
max-width: 50px; max-width: 50px;
} }
.node-cell.remaining {
min-width: 65px;
max-width: 65px;
}
.node-cell.remaining .additional {
position: absolute;
left: 0;
font-size: 12px;
text-align: center;
width: 100%;
}
.node-cell.remaining .additional small{
white-space: nowrap;
font-size: 80%;
}
.temp-detail { .temp-detail {
cursor: pointer; cursor: pointer;
} }
@@ -331,6 +349,8 @@ td.ping-network-quality {
padding: 5px 0px 15px 5px; padding: 5px 0px 15px 5px;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
border-bottom-width: 1px;
border-bottom-style: solid;
} }
.network-box .network-box-header .dropdown-menu { .network-box .network-box-header .dropdown-menu {
@@ -381,6 +401,7 @@ td.ping-network-quality {
} }
.network-box .chartTitle { .network-box .chartTitle {
cursor: pointer;
text-align: center; text-align: center;
font-size: 18px; font-size: 18px;
margin: 18px 0px 15px 0px; margin: 18px 0px 15px 0px;
@@ -483,6 +504,10 @@ footer p{
min-height: calc(100vh - 90px); min-height: calc(100vh - 90px);
min-height: calc(var(--vh, 1vh) * 100 - 90px); min-height: calc(var(--vh, 1vh) * 100 - 90px);
} }
#chartbox {
min-height: calc(100vh - 170px);
min-height: calc(var(--vh, 1vh) * 100 - 170px);
}
.content { .content {
padding: 0; padding: 0;
margin-bottom: 10px; margin-bottom: 10px;
@@ -524,6 +549,10 @@ footer p{
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.node-cell.remaining {
min-width: 75px;
max-width: 75px;
}
.accordian-body{ .accordian-body{
margin: 5px 0px 5px 10px; margin: 5px 0px 5px 10px;
} }
@@ -567,6 +596,7 @@ footer p{
.network-box .network-box-header { .network-box .network-box-header {
margin: 8px 0px 0px 8px; margin: 8px 0px 0px 8px;
font-size: 16px; font-size: 16px;
border-bottom: none;
} }
.network-box .chartTitle { .network-box .chartTitle {
font-size: 16px; font-size: 16px;
+5
View File
@@ -155,6 +155,11 @@ const mixinsVue = {
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备 checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
return window.innerWidth <= 768; return window.innerWidth <= 768;
}, },
isMenuActive(page){
if(page == this.$root.page) {
return this.isMobile ? 'm-active' : 'pc-active';
}
},
setBenchmarkHeight() { setBenchmarkHeight() {
let vh = window.innerHeight * 0.01; let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`); document.documentElement.style.setProperty('--vh', `${vh}px`);
+1 -1
View File
@@ -10,7 +10,7 @@
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.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="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> <script>
(function () { (function () {
updateLang({{.LANG }}); updateLang({{.LANG }});
+4
View File
@@ -56,6 +56,10 @@
<label>{{tr "Note"}}</label> <label>{{tr "Note"}}</label>
<textarea name="Note"></textarea> <textarea name="Note"></textarea>
</div> </div>
<div class="field">
<label>{{tr "PublicNote"}}</label>
<textarea name="PublicNote"></textarea>
</div>
<div class="command field"> <div class="command field">
<label>{{tr "LinuxOneKeyInstall"}}</label> <label>{{tr "LinuxOneKeyInstall"}}</label>
<div class="ui message"> <div class="ui message">
+12 -8
View File
@@ -30,12 +30,10 @@
<th>{{tr "VersionNumber"}}</th> <th>{{tr "VersionNumber"}}</th>
<th>{{tr "HideForGuest"}}</th> <th>{{tr "HideForGuest"}}</th>
<th>{{tr "EnableDDNS"}}</th> <th>{{tr "EnableDDNS"}}</th>
<th>{{tr "EnableIPv4"}}</th>
<th>{{tr "EnableIpv6"}}</th>
<th>{{tr "DDNSDomain"}}</th>
<th>{{tr "Secret"}}</th> <th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th> <th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th> <th>{{tr "Note"}}</th>
<th>{{tr "PublicNote"}}</th>
<th>{{tr "Administration"}}</th> <th>{{tr "Administration"}}</th>
</tr> </tr>
</thead> </thead>
@@ -50,9 +48,6 @@
<td>{{$server.Host.Version}}</td> <td>{{$server.Host.Version}}</td>
<td>{{$server.HideForGuest}}</td> <td>{{$server.HideForGuest}}</td>
<td>{{$server.EnableDDNS}}</td> <td>{{$server.EnableDDNS}}</td>
<td>{{$server.EnableIPv4}}</td>
<td>{{$server.EnableIpv6}}</td>
<td>{{$server.DDNSDomain}}</td>
<td> <td>
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}"> <button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
<i class="copy icon"></i> <i class="copy icon"></i>
@@ -75,13 +70,22 @@
<i class="apple icon"></i> <i class="apple icon"></i>
</button> </button>
</td> </td>
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.Note}}</td> <td>
{{if $server.Note}}
<button class="ui icon green mini button" title="{{ $server.Note }}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky note icon"></i></button>
{{end}}
</td>
<td>
{{if $server.PublicNote}}
<button class="ui icon green mini button" title="{{$server.PublicNote}}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky clipboard icon"></i></button>
{{end}}
</td>
<td> <td>
<div class="ui mini icon buttons"> <div class="ui mini icon buttons">
<button class="ui button" onclick="connectToServer({{$server.ID}})"> <button class="ui button" onclick="connectToServer({{$server.ID}})">
<i class="terminal icon"></i> <i class="terminal icon"></i>
</button> </button>
<button class="ui button" onclick="addOrEditServer({{$server.Marshal}})"> <button class="ui button" onclick="addOrEditServer({{$server.MarshalForDashboard}})">
<i class="edit icon"></i> <i class="edit icon"></i>
</button> </button>
<button class="ui 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/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.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/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> <script>
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
</script> </script>
+2 -2
View File
@@ -7,7 +7,7 @@
</a> </a>
<a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a> <a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
<template v-if="isMobile"> <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="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"> <div class="menu">
<a href="/service" class="item"><i class="rss icon"></i>{{tr "Services" }}</a> <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> <a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
</template> </template>
{{ if not .Conf.DisableSwitchTemplateInFrontend }} {{ 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="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"> <div class="menu">
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item"> <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> </div>
<div class="ui container"> <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>
</div> </div>
+4 -4
View File
@@ -26,10 +26,10 @@
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script> <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/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.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?v20240929">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011"> <link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202409029">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811"> <link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240916">
<script src="/static/theme-server-status/js/mixin.js?v20240907"></script> <script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
+25 -2
View File
@@ -6,6 +6,7 @@
<th class="node-cell name center">{{tr "Name"}}</th> <th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th> <th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th> <th class="node-cell location center">{{tr "Location"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">{{tr "Price"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th> <th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th> <th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th> <th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
@@ -13,12 +14,13 @@
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th> <th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th> <th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th> <th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining center">{{tr "Remaining"}}</th>
</tr> </tr>
</thead> </thead>
<tbody id="servers"> <tbody id="servers">
<template v-for="(node,index) in nodesNoTag"> <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'" <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"> <td class="node-cell status center">
<div class="status-container"> <div class="status-container">
<div v-if="node.online" class="status-icon online"></div> <div v-if="node.online" class="status-icon online"></div>
@@ -36,6 +38,12 @@
<i :class="'fi fi-' + node.location"></i> <i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">@#node.location#@</span> <span class="node-cell-location-text text-uppercase">@#node.location#@</span>
</td> </td>
<td v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">
<template v-if="additional[node.ID] && additional[node.ID].price">
<span v-if="additional[node.ID].price.amount == 0" class="node-cell-price-text">Free</span>
<span v-else class="node-cell-price-text">@#additional[node.ID].price.amount#@@#(additional[node.ID].price.cycle ? '/' + additional[node.ID].price.cycle : '')#@</span>
</template>
</td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td> <td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
<td style="text-align: center;" class="node-cell load">@#node.load#@</td> <td style="text-align: center;" class="node-cell load">@#node.load#@</td>
<td style="text-align: center;" class="node-cell network">@#node.network#@</td> <td style="text-align: center;" class="node-cell network">@#node.network#@</td>
@@ -59,6 +67,21 @@
</div> </div>
</div> </div>
</td> </td>
<td v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="additional[node.ID] && additional[node.ID].remaining.format">
<div :style="additional[node.ID].remaining.format.style" :class="additional[node.ID].remaining.days == 'lifetime' ? '' : additional[node.ID].remaining.format.class"></div>
<div class="additional">
<small v-if="additional[node.ID].remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="additional[node.ID].remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#additional[node.ID].remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#additional[node.ID].remaining.percent#@%</span>
</small>
</div>
</template>
</div>
</td>
</tr> </tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'"> <tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16"> <td colspan="16">
@@ -143,7 +166,7 @@
@#node.host.Version#@ @#node.host.Version#@
</span> </span>
<span class="node-echarts-expand"> <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> </span>
</div> </div>
</div> </div>
+25 -2
View File
@@ -9,6 +9,7 @@
<th class="node-cell name center">{{tr "Name"}}</th> <th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th> <th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th> <th class="node-cell location center">{{tr "Location"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">{{tr "Price"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th> <th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th> <th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th> <th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
@@ -16,12 +17,13 @@
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th> <th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th> <th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th> <th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining center">{{tr "Remaining"}}</th>
</tr> </tr>
</thead> </thead>
<tbody id="servers"> <tbody id="servers">
<template v-for="(node,index) in group.data"> <template v-for="(node,index) in group.data">
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle" <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"> <td class="node-cell status center">
<div class="status-container"> <div class="status-container">
<div v-if="node.online" class="status-icon online"></div> <div v-if="node.online" class="status-icon online"></div>
@@ -39,6 +41,12 @@
<i :class="'fi fi-' + node.location"></i> <i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span> <span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span>
</td> </td>
<td v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">
<template v-if="additional[node.ID] && additional[node.ID].price">
<span v-if="additional[node.ID].price.amount == 0" class="node-cell-price-text">Free</span>
<span v-else class="node-cell-price-text">@#additional[node.ID].price.amount#@@#(additional[node.ID].price.cycle ? '/' + additional[node.ID].price.cycle : '')#@</span>
</template>
</td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td> <td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
<td style="text-align: center;" class="node-cell load">@#node.load#@</td> <td style="text-align: center;" class="node-cell load">@#node.load#@</td>
<td style="text-align: center;" class="node-cell network">@#node.network#@</td> <td style="text-align: center;" class="node-cell network">@#node.network#@</td>
@@ -62,6 +70,21 @@
</div> </div>
</div> </div>
</td> </td>
<td v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="additional[node.ID] && additional[node.ID].remaining.format">
<div :style="additional[node.ID].remaining.format.style" :class="additional[node.ID].remaining.days == 'lifetime' ? '' : additional[node.ID].remaining.format.class"></div>
<div class="additional">
<small v-if="additional[node.ID].remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="additional[node.ID].remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#additional[node.ID].remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#additional[node.ID].remaining.percent#@%</span>
</small>
</div>
</template>
</div>
</td>
</tr> </tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'"> <tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16"> <td colspan="16">
@@ -146,7 +169,7 @@
@#node.host.Version#@ @#node.host.Version#@
</span> </span>
<span class="node-echarts-expand"> <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> </span>
</div> </div>
</div> </div>
+489 -88
View File
@@ -42,6 +42,7 @@
servers: [], servers: [],
nodesTag: [], nodesTag: [],
nodesNoTag: [], nodesNoTag: [],
additional: {},
chartDataList: [], chartDataList: [],
ws: null, ws: null,
language: {{.Conf.Language}}, language: {{.Conf.Language}},
@@ -81,7 +82,8 @@
this.countryNameMap = this.initCountryNameMap(); this.countryNameMap = this.initCountryNameMap();
this.countryServer = this.initCountryServer(); this.countryServer = this.initCountryServer();
this.countryMapChartData = this.initCountryMapChartData(); this.countryMapChartData = this.initCountryMapChartData();
}) });
this.additional = this.initAdditional(this.servers);
}, },
mounted() { mounted() {
// 初始化时建立WebSocket连接 // 初始化时建立WebSocket连接
@@ -98,6 +100,25 @@
}); });
}, },
methods: { methods: {
initAdditional(servers) {
let nodes = {};
servers?.forEach(server => {
if(server.PublicNote) {
nodes[server.ID] = {
"remaining": {
format: this.getRemainingFormat(server.live, server.PublicNote),
days: this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),server.PublicNote),
percent: this.toFixed2(100 - this.getRemainingPercent(this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"), this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote))
},
"price": {
amount: this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount"),
cycle: this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle")
}
};
}
});
return nodes;
},
initCountryMap() { initCountryMap() {
return fetch(this.staticUrl + '/maps/nezha.countrymap.json') return fetch(this.staticUrl + '/maps/nezha.countrymap.json')
.then(response => response.json()) .then(response => response.json())
@@ -483,24 +504,27 @@
// 如果所有元素的 Temperature 都为 0,则返回一个默认值 0 // 如果所有元素的 Temperature 都为 0,则返回一个默认值 0
return 0; return 0;
}, },
showCharts(event, id) { showCharts(id,changeChartboxShow=true) {
const chartContainer = this.$refs[`chart${id}`][0]; const chartContainer = document.getElementById(`chart-${id}`);
const chartboxShow = chartContainer.getAttribute('chartbox-show'); if(changeChartboxShow){
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0'); const chartboxShow = chartContainer.getAttribute('chartbox-show');
const isAriaExpandedFalse = event.currentTarget.getAttribute('aria-expanded') === 'false'; chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
if (!isAriaExpandedFalse) return; const collapseContainer = document.getElementById(`r${id}`);
const isAriaExpandedFalse = collapseContainer.getAttribute('aria-expanded') === 'false';
if (!isAriaExpandedFalse) return;
}
// 发起数据请求 // 发起数据请求
const url = `/api/v1/monitor/${id}`; const url = `/api/v1/monitor/${id}`;
fetch(url) fetch(url)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.result) { // 数据请求成功,更新数据并渲染图表 if (data.result) { // 数据请求成功,更新数据并渲染图表
this.chartDataList[id - 1] = data.result; this.chartDataList[id] = data.result;
this.$nextTick(() => { this.$nextTick(() => {
this.renderCharts(id); this.renderCharts(id);
}); });
} else { } else {
console.log('this agent (id:'+ id + ') has no monitor.'); console.log('this server (id:'+ id + ') has no monitor.');
} }
}) })
.catch(error => { .catch(error => {
@@ -508,22 +532,18 @@
}); });
}, },
renderCharts(id, reload = false) { renderCharts(id, reload = false) {
if (!this.chartDataList[id - 1]) return; if (!this.chartDataList[id]) return;
const chartData = this.chartDataList[id - 1]; const chartData = this.chartDataList[id];
const chartContainer = this.$refs[`chart${id}`][0]; const chartContainer = document.getElementById(`chart-${id}`);
if (reload) { //点击切换亮色/暗色风格模式时,重新载入echarts图表的逻辑, if (reload) {
// 第一步,查找已经渲染出的图表容器,并销毁它
const existingChart = echarts.getInstanceByDom(chartContainer); const existingChart = echarts.getInstanceByDom(chartContainer);
if (existingChart) existingChart.dispose(); if (existingChart) existingChart.dispose();
// 第二步,如果图表容器处于不可见状态chartboxShow=0,不重新渲染出新的图表,
// 如果图表容器处于可见状态chartboxShow=1,重新渲染出新的图表
const chartboxShow = chartContainer.getAttribute('chartbox-show'); const chartboxShow = chartContainer.getAttribute('chartbox-show');
if ( chartboxShow === '0' ) return; if ( chartboxShow === '0' ) return;
} }
// 定义图表参数值
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300; const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
const fontSize = this.isMobile ? 10 : 14; 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 gridRight = this.isMobile ? 5 : 20;
const legendLeft = this.isMobile ? 'center' : 'center'; const legendLeft = this.isMobile ? 'center' : 'center';
const legendTop = this.isMobile ? 5 : 5; const legendTop = this.isMobile ? 5 : 5;
@@ -537,47 +557,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 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 lineStyleWidth = this.isMobile ? 1 : 2;
const splitLineWidth = this.isMobile ? 0.5 : 1; const splitLineWidth = this.isMobile ? 0.5 : 1;
// 渲染图表 const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
const chart = echarts.init(chartContainer, chartTheme, { const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
const chart = echarts.init(chartContainer, chartTheme, { // init图表
renderer: 'canvas', renderer: 'canvas',
useDirtyRect: false, useDirtyRect: false,
width: 'auto', width: 'auto',
height: 300, height: 300
}); });
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString()); let legendData = [];
const seriesData = chartData.map(item => { let seriesData = [];
chartData.forEach((item,key)=> {
let loss = 0; let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => { let totalLossRate = 0;
if(avgDelay > 0 && avgDelay < MaxTCPPingValue){ let legendName = '';
loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0; let data = { main: [], markLine: []};
return [item.created_at[index], avgDelay.toFixed(2)]; item.avg_delay.forEach((avgDelay, index) => {
}else{ const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
loss += 1; 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); totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`; legendName = `${item.monitor_name} ${totalLossRate}%`;
return { legendData.push(legendName);
name: item.monitor_name, seriesData.push(
type: 'line', {
smooth: true, name: legendName,
symbol: 'none', type: 'line',
data: data, smooth: true,
connectNulls: true, symbol: 'none',
legendHoverLink: false, connectNulls: true,
emphasis: { legendHoverLink: false,
disabled: true emphasis: {
}, disabled: true
lineStyle: { },
width: lineStyleWidth 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 maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6; 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 height = 300 + autoIncrement;
const gridTop = 40 + autoIncrement; const gridTop = 40 + autoIncrement;
const legendIcon = this.isMobile ? 'rect' : ""; const legendIcon = this.isMobile ? 'rect' : "";
@@ -587,39 +651,24 @@
width: 'auto', width: 'auto',
height: height height: height
}); });
const option = { const option = {
color: this.colors,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: gridTop,
left: gridLeft,
right: gridRight,
},
title: { title: {
show: false 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,
}, },
series: seriesData.flat(),
xAxis: { xAxis: {
type: 'time', type: 'time',
data: xAxisData,
axisLabel: { axisLabel: {
textStyle: { textStyle: {
fontSize: fontSize fontSize: fontSize
@@ -639,34 +688,386 @@
} }
} }
}, },
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: [ dataZoom: [
{ {
type: 'slider', type: 'slider',
start: 0, start: 0,
end: 100 end: 100
} }
], ]
series: seriesData,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: gridTop,
left: gridLeft,
right: gridRight
}
}; };
chart.setOption(option); chart.setOption(option);
}, },
reloadCharts() { // 重新加载所有图表 reloadCharts() {
this.servers.forEach(node => { this.servers.forEach(node => {
const id = node.ID; const id = node.ID;
const chartData = this.chartDataList[id - 1]; const chartData = this.chartDataList[id];
if (chartData) { if (chartData) {
this.renderCharts(id,true); this.renderCharts(id,true);
} }
}); });
},
getNoteElementValue(string, elementName, childElementName) {
let obj;
try {
obj = JSON.parse(string);
} catch (e) {
return null;
}
if (!elementName) return null;
if (childElementName) {
return obj[elementName] && childElementName in obj[elementName] ? obj[elementName][childElementName] : null;
} else {
return elementName in obj ? obj[elementName] : null;
}
},
getBillingCycle(billingCycle) {
// 统一转换为小写进行比较
const cycle = billingCycle.toLowerCase();
switch (cycle) {
case '月':
case 'month':
case 'monthly':
case 'm':
return "M";
case '季':
case 'quarterly':
case 'q':
return "Q";
case '半':
case '半年':
case 'half':
case 'semi-annually':
case 'h':
return "H";
case '年':
case 'year':
case 'annually':
case 'y':
return "Y";
default:
return null;
}
},
getAdjustTimezone(reference, target) {
// 获取时区
const referenceTimezoneOffset = reference.getTimezoneOffset();
const targetTimezoneOffset = target.getTimezoneOffset();
// 计算时区差异
const timezoneDifference = (referenceTimezoneOffset - targetTimezoneOffset) * 60 * 1000;
// 将 target 日期调整到 reference 时区
return new Date(target.getTime() + timezoneDifference);
},
getAutoRenewalEndDate(endDate, billingCycle) {
const expiration = new Date(endDate);
const current = this.getAdjustTimezone(new Date(endDate), new Date());
// 如果 expiration 无效,返回 null 并记录日志
if (isNaN(expiration.getTime())) {
console.error("getAutoRenewalEndDate: Invalid expiration format");
}
const result = {
flag: 1, // 1表示需要更新
check: 0, // 判断逻辑标记
count: 0 // 周期累计计数
};
// 如果当前时间还没到到期时间,直接返回到期时间
if (current < expiration) {
result.flag = 0;
result.date = expiration;
return result;
}
let nextExpiration = new Date(expiration); // 初始化为原到期时间
let newExpirationMonth = expiration.getMonth(); // 获取expiration初始月份
switch (billingCycle.toUpperCase()) {
case 'M': // 月度
nextExpiration.setFullYear(current.getFullYear());
const monthCheck = current < new Date(current.getFullYear(), current.getMonth(), expiration.getDate());
// 检查当前月是否在有效期内
if (monthCheck) {
nextExpiration.setMonth(current.getMonth());
result.check = 1;
} else {
nextExpiration.setMonth(current.getMonth() + 1);
result.check = 2;
}
break;
case 'Q': // 季度
nextExpiration.setFullYear(current.getFullYear());
// 每次增加 3 个月,直到新的月份大于当前月份
while (newExpirationMonth < current.getMonth()) {
newExpirationMonth += 3;
result.count += 1;
}
// nextExpiration设置获取到的新月份
nextExpiration.setMonth(newExpirationMonth);
const quarterlyCheck = current < nextExpiration;
// 检查新月份是否在有效期内
if (quarterlyCheck) {
result.check = 3;
} else {
nextExpiration.setMonth(current.getMonth() + 3);
result.check = 4;
result.count += 1;
}
break;
case 'H': // 半年
nextExpiration.setFullYear(current.getFullYear());
// 每次增加 6 个月,直到新的月份大于当前月份
while (newExpirationMonth < current.getMonth()) {
newExpirationMonth += 6;
result.count += 1;
}
// nextExpiration设置获取到的新月份
nextExpiration.setMonth(newExpirationMonth);
const halfCheck = current < nextExpiration;
// 检查新月份是否在有效期内
if (halfCheck) {
result.check = 5;
} else {
nextExpiration.setMonth(current.getMonth() + 6);
result.check = 6;
result.count += 1;
}
break;
case 'Y': // 年度
const yearCheck = current < new Date(current.getFullYear(), expiration.getMonth(), expiration.getDate());
// 如果当前时间比这一年有效期早,则到期为本年
if (yearCheck) {
nextExpiration.setFullYear(current.getFullYear());
result.check = 7;
} else {
// 否则推到下一年
nextExpiration.setFullYear(current.getFullYear() + 1);
result.check = 8;
}
break;
default:
throw new Error("Invalid billing cycle");
}
// 保持原到期时间的时分秒
nextExpiration.setHours(expiration.getHours());
nextExpiration.setMinutes(expiration.getMinutes());
nextExpiration.setSeconds(expiration.getSeconds());
result.date = nextExpiration;
return result;
},
getAutoRenewalStartDate(flag, startDate, check, count) {
//1.判断什么时候改变 2.如何改变
const start = new Date(startDate);
const current = this.getAdjustTimezone(start, new Date());
// 检查 startDate 格式是否有效,若无效返回 null
if (isNaN(start.getTime())) {
console.error("getAutoRenewalStartDate: Invalid startDate format");
}
// 如果 flag 为 0,直接返回开始日期
if (flag === 0) {
return start;
}
// 初始化新的开始日期
const newStart = new Date(start);
switch (check) {
case 1: // 处理月份:设置为上个月
newStart.setFullYear(current.getFullYear());
newStart.setMonth(current.getMonth() - 1);
break;
case 2: // 处理月份:设置为当前月
newStart.setFullYear(current.getFullYear());
newStart.setMonth(current.getMonth());
break;
case 3:
case 4: // 处理季度
newStart.setFullYear(current.getFullYear());
newStart.setMonth(start.getMonth() + 3 * count);
break;
case 5:
case 6: // 处理半年
newStart.setFullYear(current.getFullYear());
newStart.setMonth(start.getMonth() + 6 * count);
break;
case 7: // 处理年份:设置为上一年
newStart.setFullYear(current.getFullYear() - 1);
break;
case 8: // 处理年份:保持当前年份
newStart.setFullYear(current.getFullYear());
break;
default: // 默认处理:直接返回
return;
}
return newStart;
},
getRemainingFormat(online, note) {
if (!note) return null;
const startDate = this.getNoteElementValue(note, "billingDataMod", "startDate");
const endDate = this.getNoteElementValue(note, "billingDataMod", "endDate");
// 检查 startDate 和 endDate 是否有效
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
console.error("getRemainingFormat: Invalid startDate or endDate in note");
return null; // 如果无效,返回 null 或其他错误处理逻辑
}
//处理特殊时间格式
if (startDate.includes('0000-00-00') || endDate.includes("0000-00-00")) {
return this.formatPercents(online, this.toFixed2(100));
} else {
const percent = this.getRemainingPercent(startDate, endDate, note);
return this.formatPercents(online, this.toFixed2(percent));
}
},
getRemainingDays(endDate, note) {
// 检查 endDate 是否有效
if (!endDate || typeof endDate !== 'string') {
console.error("getRemainingDays: Invalid endDate format");
return null;
}
// 处理特殊时间格式
if (endDate.includes("0000-00-00")) return "lifetime";
// 获取当前时间,并调整时区
const currentTime = this.getAdjustTimezone(new Date(endDate), new Date());
// 获取计费周期和自动续订日期
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
const autoEndDate = this.getAutoRenewalEndDate(
this.getNoteElementValue(note, "billingDataMod", "endDate"),
this.getBillingCycle(billingCycle)
);
// 检查 autoRenewal 状态
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
// 确定到期时间
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
// 计算剩余天数
const timeDiff = end - currentTime;
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
return daysDiff;
},
getRemainingPercent(startDate, endDate, note) {
// 检查 startDate 和 endDate 是否为有效字符串并处理特殊格式
if (typeof startDate !== 'string' || typeof endDate !== 'string') {
console.error("getRemainingPercent: Invalid startDate or endDate format");
return null;
}
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) return 100;
// 获取当前时间并调整时区
const now = this.getAdjustTimezone(new Date(endDate), new Date());
// 获取计费周期
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
// 自动获取结束日期
const autoEndDate = this.getAutoRenewalEndDate(
this.getNoteElementValue(note, "billingDataMod", "endDate"),
this.getBillingCycle(billingCycle)
);
// 自动获取开始日期
const autoStartDate = autoEndDate.flag == 1
? this.getAutoRenewalStartDate(autoEndDate.flag, this.getNoteElementValue(note, "billingDataMod", "startDate"), autoEndDate.check, autoEndDate.count)
: new Date(startDate);
// 计算开始和结束时间
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
const start = autoRenewal ? autoStartDate : new Date(startDate);
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
// 计算剩余百分比
if (now < start) {
return 0;
}
if (now >= end) {
return 100;
}
const totalDuration = end - start;
const elapsedDuration = now - start;
const percent = (elapsedDuration / totalDuration) * 100;
// 确保百分比在 0-100 之间
return Math.min(Math.max(percent, 0), 100);
} }
} }
}) })
+3 -14
View File
@@ -15,20 +15,9 @@
</div> </div>
<nav id="navbar" class="navbar-collapse collapse"> <nav id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li> <li :class="isMenuActive('index')"><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
<template v-if="isMobile"> <li :class="isMenuActive('service')"><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li class="dropdown"> <li :class="isMenuActive('network')"><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
<a data-toggle="dropdown"><i class="bi bi-gear-wide-connected" style="position:relative;top:1px;margin-right:3px;font-size:1.1rem;"></i>{{tr "Feature" }}<b class="caret"></b></a>
<ul class="dropdown-menu" style="min-width:100px;">
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
</ul>
</li>
</template>
<template v-else>
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
</template>
{{ if not .Conf.DisableSwitchTemplateInFrontend }} {{ if not .Conf.DisableSwitchTemplateInFrontend }}
<li class="dropdown"> <li class="dropdown">
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a> <a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
+36 -33
View File
@@ -15,7 +15,7 @@
</li> </li>
</ul> </ul>
</div> </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 id="chartbox" style="width:100%;height:auto;"></div>
</div> </div>
{{template "theme-server-status/footer" .}} {{template "theme-server-status/footer" .}}
@@ -32,7 +32,8 @@
chartTitle: '', chartTitle: '',
chartCountryCode: '', chartCountryCode: '',
chart: null, chart: null,
currentServerId: '' currentServerId: '',
nextServerId: '',
}, },
mixins: [mixinsVue], mixins: [mixinsVue],
created() { created() {
@@ -66,13 +67,13 @@
if(!this.chartDataList[id]) return; if(!this.chartDataList[id]) return;
if(this.chart) this.disposeCharts(this.chart); if(this.chart) this.disposeCharts(this.chart);
this.currentServerId = id; this.currentServerId = id;
this.nextServerId = this.getNextServerId(id);
this.chartCountryCode = this.getServerCountryCode(id); this.chartCountryCode = this.getServerCountryCode(id);
this.chartTitle = this.chartDataList[id][0].server_name; this.chartTitle = this.chartDataList[id][0].server_name;
const chartData = this.chartDataList[id]; const chartData = this.chartDataList[id];
const chartContainer = document.getElementById('chartbox'); const chartContainer = document.getElementById('chartbox');
// 定义图表参数值
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300; 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 fontSize = this.isMobile ? 10 : 14;
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 30 : 36); const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 30 : 36);
const gridRight = this.isMobile ? 12 : 20; 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 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 lineStyleWidth = this.isMobile ? 1 : 2;
const splitLineWidth = this.isMobile ? 0.5 : 1; const splitLineWidth = this.isMobile ? 0.5 : 1;
const markPointSymbolSize = this.isMobile ? 30 : 42; const markPointSymbolSize = this.isMobile ? 36 : 42;
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 0.9; const markPointItemStyleOpacity = this.semiTransparent ? 1 : 1;
const markPointFontSize = this.isMobile ? 8 : 10; const markPointFontSize = this.isMobile ? 8 : 10;
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75; const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3; 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图表 this.chart = echarts.init(chartContainer, chartTheme, { // init图表
renderer: 'canvas', renderer: 'canvas',
useDirtyRect: false, useDirtyRect: false,
width: 'auto', width: 'auto',
height: autoheight, height: autoheight,
}); });
// 获取图表数据 this.chart.showLoading({
text: 'loading',
textColor: showLoadingTextColor,
color: showLoadingColor,
maskColor: showLoadingMaskColor,
zlevel: 2
});
let legendData = []; let legendData = [];
let seriesData = []; let seriesData = [];
chartData.forEach((item,key)=> { chartData.forEach((item,key)=> {
@@ -112,9 +122,6 @@
loss += 1; loss += 1;
const lossrate = 100 * loss / (index + 1); const lossrate = 100 * loss / (index + 1);
if(lossrate != 100) { if(lossrate != 100) {
data['main'].push(
[item.created_at[index], MaxTCPPingValue, lossrate]
);
data['markLine'].push({ data['markLine'].push({
xAxis: item.created_at[index], xAxis: item.created_at[index],
label: { show: false }, label: { show: false },
@@ -143,11 +150,9 @@
); );
} }
}); });
// 处理legendData
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1); totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
legendName = `${item.monitor_name} ${totalLossRate}%`; legendName = `${item.monitor_name} ${totalLossRate}%`;
legendData.push(legendName); legendData.push(legendName);
// 处理seriesData
seriesData.push( seriesData.push(
{ {
name: legendName, name: legendName,
@@ -228,29 +233,22 @@
}); });
// 设置图表配置项 // 设置图表配置项
const option = { const option = {
// 全局调色盘
color: this.colors, color: this.colors,
// 背景颜色
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
// 文字样式
textStyle: { textStyle: {
fontSize: fontSize, fontSize: fontSize,
color: fontColor color: fontColor
}, },
// 图表网格设置
grid: { grid: {
top: gridTop, top: gridTop,
left: gridLeft, left: gridLeft,
right: gridRight, right: gridRight,
bottom: gridBottom bottom: gridBottom
}, },
// 图表标题设置
title: { title: {
show: false, show: false,
}, },
// 图表系列数据设置
series: seriesData.flat(), series: seriesData.flat(),
// X轴设置
xAxis: { xAxis: {
type: 'time', type: 'time',
axisLabel: { axisLabel: {
@@ -259,7 +257,6 @@
} }
} }
}, },
// Y轴设置
yAxis: { yAxis: {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
@@ -273,7 +270,6 @@
} }
} }
}, },
// 图例设置
legend: { legend: {
data: legendData, data: legendData,
show: true, show: true,
@@ -289,7 +285,6 @@
itemWidth: itemWidth, itemWidth: itemWidth,
itemHeight: itemHeight, itemHeight: itemHeight,
}, },
// 提示框设置
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: tooltipBackgroundColor, backgroundColor: tooltipBackgroundColor,
@@ -312,7 +307,6 @@
return tooltipContent; return tooltipContent;
} }
}, },
// 数据缩放设置
dataZoom: [ dataZoom: [
{ {
type: 'slider', type: 'slider',
@@ -321,17 +315,16 @@
} }
] ]
}; };
// 设置图表的配置选项 setTimeout(() => {
this.chart.setOption(option); this.chart.hideLoading();
this.chart.setOption(option);
}, 1000);
}, },
reloadCharts() { // 重新加载所有图表 reloadCharts() {
this.servers.forEach(node => { const chartData = this.chartDataList[this.currentServerId];
const id = node.ID; if (chartData) {
const chartData = this.chartDataList[id]; this.renderCharts(this.currentServerId,true);
if (chartData) { }
this.renderCharts(id,true);
}
});
}, },
disposeCharts(chart){ disposeCharts(chart){
chart.dispose(); chart.dispose();
@@ -341,6 +334,16 @@
const result = this.servers.find(item => item.ID == id); const result = this.servers.find(item => item.ID == id);
return result.Host.CountryCode ? result.Host.CountryCode : 'rb'; 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() { initSearch() {
$('#dropdown-search').on('keyup', function() { $('#dropdown-search').on('keyup', function() {
var searchTerm = $(this).val().toLowerCase(); var searchTerm = $(this).val().toLowerCase();
+19 -2
View File
@@ -3,6 +3,7 @@ package rpc
import ( import (
"errors" "errors"
"io" "io"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
@@ -14,6 +15,18 @@ type ioStreamContext struct {
agentIoConnectCh chan 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) { func (s *NezhaHandler) CreateStream(streamId string) {
s.ioStreamMutex.Lock() s.ioStreamMutex.Lock()
defer s.ioStreamMutex.Unlock() defer s.ioStreamMutex.Unlock()
@@ -117,7 +130,9 @@ LOOP:
endCh := make(chan struct{}) endCh := make(chan struct{})
go func() { 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 { if innerErr != nil {
err = innerErr err = innerErr
} }
@@ -126,7 +141,9 @@ LOOP:
} }
}() }()
go func() { 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 { if innerErr != nil {
err = innerErr err = innerErr
} }