Compare commits

...
8 Commits
Author SHA1 Message Date
nap0o ab39782c78 fix bug (#328)
* fix bugs

1. 修复default主题mixin.js文件丢失
2. 修复主题默认值为后台设置值
3. default主题样式优化

* 修复前台切换 theme-custom 不生效
2024-02-26 09:31:16 +08:00
github-actions[bot] e24925b049 update contributors[no ci] 2024-02-25 15:35:34 +00:00
naiba 1d4245ee09 Update mixin.js file path in header.html 2024-02-25 23:35:06 +08:00
nap0o 8df863a3e0 优化default和serverstatus主题模版 (#327)
1.优化default主题模版
2.优化serverstatus主题模版
2024-02-25 23:16:57 +08:00
naiba 07e0382598 🚸 校验自定义主题、将 Theme 列表插入所有页面 2024-02-25 14:15:19 +08:00
naiba 8dd509aa08 🔒️ 增强 ping 历史 API 鉴权 2024-02-24 23:21:33 +08:00
naiba 99ac12c9fd 前端切换模板 2024-02-24 22:28:07 +08:00
github-actions[bot] ebc45f9b8f update contributors[no ci] 2024-02-24 13:10:48 +00:00
42 changed files with 1925 additions and 582 deletions
+6 -3
View File
@@ -95,6 +95,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/iilemon" title="Sean">
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
</a>
<a href="https://github.com/nap0o" title="nap0o">
<img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
</a>
<a href="https://github.com/fscarmen" title="fscarmen">
<img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/>
</a>
@@ -122,9 +125,6 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/unclezs" title="unclezs">
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
</a>
<a href="https://github.com/nap0o" title="nap0o">
<img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
</a>
<a href="https://github.com/yuanweize" title="I">
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
</a>
@@ -161,6 +161,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/Es-dese" title="Esdese">
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
</a>
<a href="https://github.com/DarcJC" title="Darc Z.">
<img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/>
</a>
<a href="https://github.com/wwng2333" title="Crazy">
<img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/>
</a><!--GAMFC_DELIMITER_END-->
+28 -7
View File
@@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
)
@@ -16,18 +17,28 @@ type apiV1 struct {
func (v *apiV1) serve() {
r := v.r.Group("")
// API
// 强制认证的 API
r.Use(mygin.Authorize(mygin.AuthorizeOption{
Member: true,
IsPage: false,
AllowAPI: true,
Msg: "访问此接口需要认证",
Btn: "点此登录",
Redirect: "/login",
MemberOnly: true,
AllowAPI: true,
IsPage: false,
Msg: "访问此接口需要认证",
Btn: "点此登录",
Redirect: "/login",
}))
r.GET("/server/list", v.serverList)
r.GET("/server/details", v.serverDetails)
// 不强制认证的 API
mr := v.r.Group("monitor")
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
MemberOnly: false,
IsPage: false,
ValidateViewPassword: true,
AllowAPI: true,
Msg: "访问此接口需要认证",
Btn: "点此登录",
Redirect: "/login",
}))
mr.GET("/:id", v.monitorHistoriesById)
}
@@ -84,5 +95,15 @@ func (v *apiV1) monitorHistoriesById(c *gin.Context) {
})
return
}
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
authorized := isMember || isViewPasswordVerfied
if server.HideForGuest && !authorized {
c.AbortWithStatusJSON(403, gin.H{"code": 403, "message": "需要认证"})
return
}
c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
}
+7 -30
View File
@@ -43,10 +43,12 @@ type commonPage struct {
func (cp *commonPage) serve() {
cr := cp.r.Group("")
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
cr.Use(mygin.Authorize(mygin.AuthorizeOption{
ValidateViewPassword: true,
}))
cr.Use(mygin.PreferredTheme)
cr.GET("/terminal/:id", cp.terminal)
cr.POST("/view-password", cp.issueViewPassword)
cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
cr.GET("/", cp.home)
cr.GET("/service", cp.service)
// TODO: 界面直接跳转使用该接口
@@ -85,31 +87,6 @@ func (p *commonPage) issueViewPassword(c *gin.Context) {
c.Redirect(http.StatusFound, c.Request.Referer())
}
func (p *commonPage) checkViewPassword(c *gin.Context) {
if singleton.Conf.Site.ViewPassword == "" {
c.Next()
return
}
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); authorized {
c.Next()
return
}
// 验证查看密码
viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil {
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/viewpassword", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
"CustomCode": singleton.Conf.Site.CustomCode,
}))
c.Abort()
return
}
c.Set(model.CtxKeyViewPasswordVerified, true)
c.Next()
}
func (p *commonPage) service(c *gin.Context) {
res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
singleton.AlertsLock.RLock()
@@ -128,7 +105,7 @@ func (p *commonPage) service(c *gin.Context) {
stats, statsStore,
}, nil
})
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/service"), mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
"Services": res.([]interface{})[0],
"CycleTransferStats": res.([]interface{})[1],
@@ -234,7 +211,7 @@ func (cp *commonPage) network(c *gin.Context) {
Servers: servers,
})
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/network", mygin.CommonEnvironment(c, gin.H{
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
"Servers": string(serversBytes),
"MonitorInfos": string(monitorInfos),
"CustomCode": singleton.Conf.Site.CustomCode,
@@ -280,7 +257,7 @@ func (cp *commonPage) home(c *gin.Context) {
}, true)
return
}
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/home", mygin.CommonEnvironment(c, gin.H{
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
"Servers": string(stat),
"CustomCode": singleton.Conf.Site.CustomCode,
}))
+5 -5
View File
@@ -19,11 +19,11 @@ type guestPage struct {
func (gp *guestPage) serve() {
gr := gp.r.Group("")
gr.Use(mygin.Authorize(mygin.AuthorizeOption{
Guest: true,
IsPage: true,
Msg: "您已登录",
Btn: "返回首页",
Redirect: "/",
GuestOnly: true,
IsPage: true,
Msg: "您已登录",
Btn: "返回首页",
Redirect: "/",
}))
gr.GET("/login", gp.login)
+11 -9
View File
@@ -28,11 +28,11 @@ type memberAPI struct {
func (ma *memberAPI) serve() {
mr := ma.r.Group("")
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
Member: true,
IsPage: false,
Msg: "访问此接口需要登录",
Btn: "点此登录",
Redirect: "/login",
MemberOnly: true,
IsPage: false,
Msg: "访问此接口需要登录",
Btn: "点此登录",
Redirect: "/login",
}))
mr.GET("/search-server", ma.searchServer)
@@ -444,10 +444,12 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
err = singleton.DB.Save(&m).Error
}
}
if m.Cover == 0 {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
} else {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
if err == nil {
if m.Cover == 0 {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
} else {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
}
}
}
if err == nil {
+5 -6
View File
@@ -17,11 +17,11 @@ type memberPage struct {
func (mp *memberPage) serve() {
mr := mp.r.Group("")
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
Member: true,
IsPage: true,
Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
Redirect: "/login",
MemberOnly: true,
IsPage: true,
Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
Redirect: "/login",
}))
mr.GET("/server", mp.server)
mr.GET("/monitor", mp.monitor)
@@ -81,7 +81,6 @@ func (mp *memberPage) setting(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
"Languages": model.Languages,
"Themes": model.Themes,
"DashboardThemes": model.DashboardThemes,
}))
}
+1
View File
@@ -8,6 +8,7 @@ import (
const CtxKeyAuthorizedUser = "ckau"
const CtxKeyViewPasswordVerified = "ckvpv"
const CtxKeyPreferredTheme = "ckpt"
const CacheKeyOauth2State = "p:a:state"
type Common struct {
+30 -10
View File
@@ -6,25 +6,28 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/crypto/bcrypt"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/service/singleton"
)
type AuthorizeOption struct {
Guest bool
Member bool
IsPage bool
AllowAPI bool
Msg string
Redirect string
Btn string
GuestOnly bool
MemberOnly bool
ValidateViewPassword bool
IsPage bool
AllowAPI bool
Msg string
Redirect string
Btn string
}
func Authorize(opt AuthorizeOption) func(*gin.Context) {
return func(c *gin.Context) {
var code = http.StatusForbidden
if opt.Guest {
if opt.GuestOnly {
code = http.StatusBadRequest
}
@@ -67,15 +70,32 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
}
}
}
// 已登录且只能游客访问
if isLogin && opt.Guest {
if isLogin && opt.GuestOnly {
ShowErrorPage(c, commonErr, opt.IsPage)
return
}
// 未登录且需要登录
if !isLogin && opt.Member {
if !isLogin && opt.MemberOnly {
ShowErrorPage(c, commonErr, opt.IsPage)
return
}
// 验证查看密码
if opt.ValidateViewPassword && singleton.Conf.Site.ViewPassword != "" {
viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil {
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
"CustomCode": singleton.Conf.Site.CustomCode,
}))
c.Abort()
return
}
c.Set(model.CtxKeyViewPasswordVerified, true)
}
}
}
+1
View File
@@ -24,6 +24,7 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
data["MatchedPath"] = c.MustGet("MatchedPath")
data["Version"] = singleton.Version
data["Conf"] = singleton.Conf
data["Themes"] = model.Themes
// 是否是管理页面
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
// 站点标题
+30
View File
@@ -0,0 +1,30 @@
package mygin
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/service/singleton"
)
func PreferredTheme(c *gin.Context) {
// 采用前端传入的主题
if theme, err := c.Cookie("preferred_theme"); err == nil {
if _, has := model.Themes[theme]; has {
// 检验自定义主题
if theme == "custom" && singleton.Conf.Site.Theme != "custom" && !utils.IsFileExists("resource/template/theme-custom/home.html") {
return
}
c.Set(model.CtxKeyPreferredTheme, theme)
}
}
}
func GetPreferredTheme(c *gin.Context, path string) string {
if theme, has := c.Get(model.CtxKeyPreferredTheme); has {
return fmt.Sprintf("theme-%s%s", theme, path)
}
return fmt.Sprintf("theme-%s%s", singleton.Conf.Site.Theme, path)
}
+11 -2
View File
@@ -398,7 +398,7 @@ other = "Virtualization"
other = "Swap"
[NetTransfer]
other = "Network Transfer"
other = "Transfer"
[Load]
other = "Load"
@@ -419,7 +419,7 @@ other = "Last Active"
other = "Version"
[NetSpeed]
other = "Network Speed"
other = "Speed"
[Uptime]
other = "Uptime"
@@ -621,3 +621,12 @@ other = "Enable DDNS"
[DDNSDomain]
other = "DDNS Domain"
[Feature]
other = "Feature"
[Template]
other = "Template"
[Stat]
other = "Stat"
+9
View File
@@ -621,3 +621,12 @@ other = "Habilitar DDNS"
[DDNSDomain]
other = "Dominio DDNS"
[Feature]
other = "Característica"
[Template]
other = "Plantilla"
[Stat]
other = "Stat"
+9
View File
@@ -621,3 +621,12 @@ other = "启用DDNS"
[DDNSDomain]
other = "DDNS域名"
[Feature]
other = "功能"
[Template]
other = "主题"
[Stat]
other = "信息"
+9
View File
@@ -621,3 +621,12 @@ other = "啟用DDNS"
[DDNSDomain]
other = "DDNS網域"
[Feature]
other = "功能"
[Template]
other = "主題"
[Stat]
other = "信息"
+217
View File
@@ -0,0 +1,217 @@
/* 屏幕适配 */
@media only screen and (min-width:1200px) {
.ui.container {
width:95% !important;
font-size: 90% !important;
}
}
@media only screen and (max-width:767px) {
.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui) {
margin-top:0.4em !important;
}
.ui.menu .item>img:not(.ui){
width: 2.2rem;
}
.ui.menu .item:before{
width:0.5px;
}
.ui.menu .item{
padding: 0.9rem 0.55rem;
}
.ui.large.menu{
font-size: 1rem;
}
}
i.icon {
color:#000;
width:1.2em !important;
}
i.fi {
width:0.9em;
margin:0px 6px 0px 2px;
}
body {
content:" " !important;
background:fixed !important;
z-index:-1 !important;
top:0 !important;
right:0 !important;
bottom:0 !important;
left:0 !important;
}
td {
word-wrap: break-word;
word-break: break-all;
}
.nb-container {
padding-top: 75px;
min-height: 100vh;
padding-bottom: 65px;
margin-bottom: -47px;
}
#app .ui.fluid.accordion {
margin-bottom: 1rem;
}
.login.nb-container {
display: flex;
align-items: center;
padding-top: unset;
}
.login.nb-container > .grid {
width: 100%;
margin: 0 auto;
}
.login.nb-container > .grid .column {
max-width: 450px;
}
.ui.menu .item-right:before{
width:0px;
}
.status.cards .flag {
margin-right: 0 !important;
}
.status.cards .header > .info.icon {
float: right;
margin-right: 0;
}
.ui.grid {
margin-bottom:-0.5em
}
.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui){
line-height: 1em;
}
.status.cards .wide.column {
padding-top: 0 !important;
padding-bottom: 0 !important;
height:2.3rem !important;
}
.status.cards .wide.column:nth-child(1) {
margin-top:1.2rem !important;
}
.status.cards .wide.column:nth-child(2) {
margin-top:1.2rem !important;
}
.status.cards .three.wide.column {
text-align: center;
width: 22%!important;
}
.status.cards .thirteen.wide.column{
width: 78%!important;
padding-left:0;
}
.status.cards .description {
padding-bottom:0 !important;
}
.status.cards .flag {
margin-right:0.5rem !important;
}
.status.cards .header > .info.icon {
float: right;
margin-right:0 !important;
}
.ui.popup:before {
display: none;
}
.closePopup{
color:rgb(10, 148, 242) !important;
position: absolute;
top: 7px;
right: 10px;
cursor: pointer;
z-index: 9999;
}
.ui.content {
margin:0 !important;
padding:1em !important;
}
.status.cards .ui.content.popup {
min-width:calc(100%)!important;
line-height:2rem !important;
border-radius:5px !important;
border:1px solid transparent !important;
font-family:Arial,Helvetica,sans-serif !important;
}
.status.cards .outline.icon {
margin-right:1px !important;
}
.ui.progress .bar {
min-width:1.8em !important;
border-radius:5px !important;
line-height:1.65em !important;
text-align: right;
padding-right: 0.4em;
color: rgba(255, 255, 255, 0.7);
font-weight: 700;
max-width: 100% !important;
}
.service-status .delay-today {
display: flex;
align-items: center;
}
.service-status .delay-today > i {
display: inline-block;
width: 1.2em;
height: 1.2em;
border-radius: 0.6em;
background-color: grey;
margin-right: 0.3em;
}
.service-status .danger {
background-color: crimson !important;
}
.service-status .good {
background-color: rgb(10, 148, 242) !important;
}
.service-status .warning {
background-color: orange !important;
}
.nezha-primary-btn {
background-color: #0338d6 !important;
color: white !important;
}
.nezha-primary-font {
color: #0338d6 !important;
}
.nezha-secondary-font {
height: 1em;
color: rgb(10, 148, 242) !important;
}
.ui-alerts.top-center {
z-index: 99999999;
}
+56
View File
@@ -0,0 +1,56 @@
const mixinsVue = {
delimiters: ['@#', '#@'],
data: {
preferredTemplate: null,
isMobile: false
},
created() {
this.isMobile = this.checkIsMobile();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
},
methods: {
toggleTemplate(template) {
if( template != this.preferredTemplate){
this.preferredTemplate = template;
this.updateCookie("preferred_theme", template);
window.location.reload();
}
},
updateCookie(name, value) {
document.cookie = name + "=" + value +"; path=/";
},
getCookie(name) {
const cookies = document.cookie.split(';');
let cookieValue = null;
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name + '=')) {
cookieValue = cookie.substring(name.length + 1, cookie.length);
break;
}
}
return cookieValue;
},
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
return window.innerWidth <= 768;
},
logOut(id) {
$.ajax({
type: 'POST',
url: '/api/logout',
data: JSON.stringify({ id: id }),
contentType: 'application/json',
success: function (resp) {
if (resp.code == 200) {
window.location.reload();
} else {
alert('注销失败(Error ' + resp.code + '): ' + resp.message);
}
},
error: function (err) {
alert('网络错误: ' + err.responseText);
}
});
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+186 -33
View File
@@ -6,58 +6,120 @@ body {
/* 导航部分 开始*/
.navbar {
min-height: 40px !important;
}
.navbar-inner{
margin:0 auto;
font-size: 14px;
}
.pl-md-unset {
.navbar .container{
max-width: 95vw;
margin: 0 auto;
}
.navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle {
.navbar-inverse{
background-image: none;
background-color: #1C2127;
box-shadow: 0 1px 40px -8px #00000080;
}
.navbar-inverse .navbar-toggle:focus,
.navbar-inverse .navbar-toggle:hover {
background-color: #1C2127;
}
.navbar .navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle {
margin-top: 18px;
padding: 0 !important;
}
.navbar-toggle {
.navbar .navbar-toggle {
margin-right:0
}
.navbar-brand {
.navbar .navbar-brand {
font-size: 20px;
text-align: center;
padding:12px 0 0 0;
margin-right:30px;
}
.node-cell-expand {
max-width: 420px;
.navbar .node-cell-expand {
word-break: break-all;
}
.node-cell-expand-label {
margin-right: 5px;
.navbar .node-cell-expand-label {
/*margin-right: 5px;*/
}
.dropdown .dropdown-toggle {
.navbar .dropdown .dropdown-toggle {
padding-bottom: 10px;
padding-top: 10px;
}
.navbar-inverse, .nav.navbar-nav {
background-image: linear-gradient(rgb(60, 60, 60) 0px, rgb(34, 34, 34) 100%) !important;
.navbar .navbar-nav {
margin:0px -15px;
}
.navbar-inverse .navbar-nav>li>a {
.navbar .navbar-nav>li>a {
color:#f1f1f1;
}
.navbar-inverse .navbar-brand {
font-size: 20px;
.navbar-nav li a span{
margin-right: 4px;
}
.navbar .navbar-collapse{
max-height: 500px;
}
/* 导航部分 结束 */
/* toolbox 开始 */
.toolbox {
position: fixed;
bottom:20px;
right: 12px;
z-index: 999999;
}
.toolbox span{
display: block;
width: 2.75rem;
margin-bottom: 10px;
}
.toolbox i{
display: block;
color: rgba(241,241,241,1);
cursor: pointer;
border-radius: 5px;
font-size: 1.5rem;
height: 2.75rem;
width: 2.75rem;
line-height: 2.75rem;
text-align: center;
}
.toolbox .toggleView i.show-nogroup {
font-size: 1.85rem;
}
.toolbox .setTheme i.setTheme-dark {
font-size: 1.35rem;
}
.toolbox .setTheme i.setTheme-light {
font-size: 1.45rem;
}
.toolbox .showGoTop i.goTop {
font-size: 1.55rem;
}
/* toolbox 结束 */
/* 正文部分 开始 */
.content {
padding: 20px;
@@ -75,15 +137,40 @@ body {
text-align: left;
}
.table>thead>tr>th{
border:none;
.table > tbody > tr > td,
.table > tbody > tr > th,
.table > tfoot > tr > td,
.table > tfoot > tr > th,
.table > thead > tr > td,
.table > thead > tr > th {
position: relative;
border:none;
line-height:20px;
}
.table .node-group-tag {
.table > tbody > tr > td:before,
.table > tbody > tr > th:before,
.table > tfoot > tr > td:before,
.table > tfoot > tr > th:before,
.table > thead > tr > td:before,
.table > thead > tr > th:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0.7px;
}
.table .node-group-tag th{
font-size: 18px;
padding-bottom:15px;
}
.table .node-cell-os-text {
text-transform: capitalize;
}
.progress {
margin-bottom: 0;
}
@@ -93,7 +180,6 @@ body {
padding-left: 5px;
}
.expandRow > td {
padding: 0 !important;
border-top: 0 !important;
@@ -131,25 +217,49 @@ body {
}
.node-cell.network {
min-width: 110px;
max-width: 110px;
min-width: 100px;
max-width: 100px;
}
.node-cell.cpu, .node-cell.ram, .node-cell.hdd {
min-width: 45px;
max-width: 90px;
.node-cell.traffic {
min-width: 100px;
}
.node-cell.cpu, .node-cell.ram, .node-cell.hdd, .node-cell.memory {
min-width: 50px;
max-width: 50px;
}
/*正文结束*/
/* 服务页 正文*/
.service-status {
}
.service-status .service-status-th{
min-width:60px;
}
.service-status .service-name-th{
min-width:50px;
}
.service-status .service-averagelatency-th{
min-width:80px;
}
.service-status .service-30daysonline-th{
min-width:80px;
}
.service-status .delay-today {
display: flex;
align-items: center;
justify-content: left;
}
.service-status .delay-today > i {
.service-status .delay-today i {
width: 12px;
height: 12px;
border-radius: 50%;
@@ -163,26 +273,29 @@ body {
width: 18px;
height: 18px;
margin-right: 4px;
margin-bottom: -3.25px;
border-radius: 3px;
box-shadow: inset 0 2px 2px rgba(0, 0, 0, .1);
}
.service-status {
.service-status .tooltip-inner {
max-width: 500px;
}
/* 服务页 正文结束 */
@media only screen and (max-width: 1200px) {
.accordian-body{
margin: 5px 0px 5px 10px;
}
.table .node-group-tag {
.table .node-group-tag th{
font-size:16px;
padding-bottom:6px;
}
}
@media only screen and (max-width: 720px) {
@media only screen and (max-width: 767px) {
body {
font-size: 10px !important;
padding-top:60px !important;
@@ -191,6 +304,23 @@ body {
padding: 0;
margin-bottom: 10px;
}
.navbar .navbar-nav .open .dropdown-menu>li>a {
color: #f1f1f1;
}
.navbar .navbar-nav .open .dropdown-menu {
list-style-image: initial;
background-color: #181a1b;
border-color: rgba(140, 130, 115, 0.15);
box-shadow: rgba(0, 0, 0, 0.18) 0px 6px 12px;
}
.table > tbody > tr > td:before,
.table > tbody > tr > th:before,
.table > tfoot > tr > td:before,
.table > tfoot > tr > th:before,
.table > thead > tr > td:before,
.table > thead > tr > th:before {
height: 0.5px;
}
.node-cell.os,
.node-cell.uptime,
.node-cell.traffic{
@@ -208,10 +338,33 @@ body {
.accordian-body{
margin: 5px 0px 5px 10px;
}
.table .node-group-tag {
font-size:12px;
.table .node-group-tag th{
font-size:16px;
padding-bottom:6px;
}
.service-status .service-status-th{
min-width:30px;
}
.service-status .delay-today{
margin-top:4px;
justify-content: center;
}
.service-status .delay-today i{
margin-right:0px;
}
.service-status .delay-today-text{
display: none;
visibility: hidden;
}
.service-status .service-averagelatency-th{
min-width:70px;
}
.service-status .service-30daysonline-th{
min-width:75px;
}
.toolbox {
right: 18px;
}
}
@media only screen and (min-width: 768px) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+102 -10
View File
@@ -2,27 +2,74 @@ const mixinsVue = {
data: {
cache: [],
theme: "light",
isSystemTheme: false
isSystemTheme: false,
showGroup: false,
showGoTop: false,
preferredTemplate: null,
isMobile: false
},
created() {
this.initTheme()
this.isMobile = this.checkIsMobile();
this.initTheme();
this.storedShowGroup();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
window.addEventListener('scroll', this.handleScroll);
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
toggleView() {
this.showGroup = !this.showGroup;
localStorage.setItem("showGroup", JSON.stringify(this.showGroup));
return this.showGroup;
},
storedShowGroup() {
const storedShowGroup = localStorage.getItem("showGroup");
if (storedShowGroup !== null) {
this.showGroup = JSON.parse(storedShowGroup);
}
},
toggleTemplate(template) {
if( template != this.preferredTemplate){
this.preferredTemplate = template;
this.updateCookie("preferred_theme", template);
window.location.reload();
}
},
updateCookie(name, value) {
document.cookie = name + "=" + value +"; path=/";
},
getCookie(name) {
const cookies = document.cookie.split(';');
let cookieValue = null;
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name + '=')) {
cookieValue = cookie.substring(name.length + 1, cookie.length);
break;
}
}
return cookieValue;
},
setTheme(title, store = false) {
this.theme = title
document.body.setAttribute("theme", title)
this.theme = title;
document.body.setAttribute("theme", title);
if (store) {
localStorage.setItem("theme", title)
this.isSystemTheme = false
localStorage.setItem("theme", title);
this.isSystemTheme = false;
if(this.$root.page == 'index') {
this.$root.reloadCharts(); //重新载入echarts图表
}
}
},
setSystemTheme() {
localStorage.removeItem("theme")
this.initTheme()
this.isSystemTheme = true
localStorage.removeItem("theme");
this.initTheme();
this.isSystemTheme = true;
},
initTheme() {
const storeTheme = localStorage.getItem("theme")
const storeTheme = localStorage.getItem("theme");
if (storeTheme === 'dark' || storeTheme === 'light') {
this.setTheme(storeTheme, true);
} else {
@@ -45,5 +92,50 @@ const mixinsVue = {
toFixed2(f) {
return f.toFixed(2)
},
logOut(id) {
$.ajax({
type: 'POST',
url: '/api/logout',
data: JSON.stringify({ id: id }),
contentType: 'application/json',
success: function (resp) {
if (resp.code == 200) {
window.location.reload();
} else {
alert('注销失败(Error ' + resp.code + '): ' + resp.message);
}
},
error: function (err) {
alert('网络错误: ' + err.responseText);
}
});
},
goTop() {
$('html, body').animate({ scrollTop: 0 }, 400);
return false;
},
handleScroll() {
this.showGoTop = window.scrollY >= 100;
},
groupingData(data, field) {
let map = new Map();
let dest = [];
data.forEach(item => {
if (!map.has(item[field])) {
dest.push({
[field]: item[field],
data: [item]
});
map.set(item[field], item);
} else {
dest.find(dItem => dItem[field] === item[field]).data.push(item);
}
});
return dest;
},
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
return window.innerWidth <= 768;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{{define "theme-default/footer"}}
</div>
<div class="ui inverted vertical footer segment">
<div class="ui center aligned is-size-7 container">
<b>&copy; <a style="color: white;" href="/">{{.Conf.Site.Brand}}</a></b> | <small>Powered by <a
href="https://github.com/naiba/nezha" style="color: white;" target="_blank">{{tr "NezhaMonitoring"}}</a>
{{.Version}}</small>
</div>
</div>
</body>
</html>
{{end}}
+25
View File
@@ -0,0 +1,25 @@
{{define "theme-default/header"}}
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/assets/font-logos.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240226">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
<script src="/static/theme-default/js/mixin.js?v20240226"></script>
</head>
<body>
{{end}}
+234 -27
View File
@@ -1,10 +1,10 @@
{{define "theme-default/home"}}
{{template "common/header" .}}
{{template "theme-default/header" .}}
{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
{{template "common/menu" .}}
{{template "theme-default/menu" .}}
<div class="nb-container">
<div class="ui container">
<div id="app">
<div class="ui container">
<template v-if="groups">
<div class="ui styled fluid accordion" v-for="group in groups">
<div class="active title">
<i class="dropdown icon"></i>
@@ -15,12 +15,13 @@
<div v-for="server in group.data" :id="server.ID" class="ui card">
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
<div class="header">
<i :class="server.Host.CountryCode + ' flag'"></i>&nbsp;<i v-if='server.Host.Platform == "darwin"'
<i :class="'fi fi-' + server.Host.CountryCode"></i>&nbsp;<i v-if='server.Host.Platform == "darwin"'
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
<i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
<div class="ui content popup" style="margin-bottom: 0">
<i @click="togglePopup($event, server.ID)" aria-expanded="false" class="nezha-secondary-font info circle icon"></i>
<div class="ui content popup" :class="{ 'visible': isActive(server.ID) }" style="margin-bottom: 0;">
<i class="closePopup window close icon" @click="closePopup(server.ID)"></i>
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
[<span
v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
@@ -40,7 +41,8 @@
{{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
{{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
{{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
{{tr "Version"}}: @#server.Host.Version#@<br />
{{tr "Version"}}: @#server.Host.Version#@
<div class="chartbox" :key="server.ID" :ref="`chart${server.ID}`" style="width: 100%; height: auto; margin-bottom: 2px;"></div>
</div>
<div class="ui divider" style="margin-bottom: 5px"></div>
</div>
@@ -72,13 +74,6 @@
</div>
</div>
</div>
<div class="three wide column">{{tr "NetSpeed"}}</div>
<div class="thirteen wide column">
<i class="arrow alternate circle down outline icon"></i>
@#formatByteSize(server.State.NetInSpeed)#@/s
<i class="arrow alternate circle up outline icon"></i>
@#formatByteSize(server.State.NetOutSpeed)#@/s
</div>
<div class="three wide column">{{tr "DiskUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
@@ -88,6 +83,36 @@
</div>
</div>
</div>
<div class="three wide column">{{tr "NetSpeed"}}</div>
<div class="thirteen wide column">
<i class="arrow alternate circle down outline icon"></i>
@#formatByteSize(server.State.NetInSpeed)#@/s
<i class="arrow alternate circle up outline icon"></i>
@#formatByteSize(server.State.NetOutSpeed)#@/s
</div>
<div class="three wide column">{{tr "NetTransfer"}}</div>
<div class="thirteen wide column">
<i class="arrow circle down icon"></i>
@#formatByteSize(server.State.NetInTransfer)#@
&nbsp;
<i class="arrow circle up icon"></i>
@#formatByteSize(server.State.NetOutTransfer)#@
</div>
<div class="three wide column">{{tr "Stat"}}</div>
<div class="thirteen wide column">
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
&nbsp;
<i class="bi bi-memory" style="font-size: 1.1rem; color: #00ac0d;"></i> @#getK2Gb(server.Host.MemTotal)#@
&nbsp;
<i class="bi bi-hdd" style="font-size: 1.1rem; color: #e41e10"></i> @#getK2Gb(server.Host.DiskTotal)#@
</div>
<div class="three wide column">{{tr "Load"}}</div>
<div class="thirteen wide column">
<i class="bi bi-activity" style="font-size: 1.1rem; color: #e41e10;"></i>
@# toFixed2(server.State.Load1) #@ |
@# toFixed2(server.State.Load5) #@ |
@# toFixed2(server.State.Load15) #@
</div>
<div class="three wide column">{{tr "Uptime"}}</div>
<div class="thirteen wide column">
<i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
@@ -103,30 +128,172 @@
</div>
</div>
</div>
</div>
</template>
</div>
</div>
{{template "common/footer" .}}
{{template "theme-default/footer" .}}
<script>
const initData = JSON.parse('{{.Servers}}').servers;
var statusCards = new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
data: initData,
page: 'index',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
data: [],
groups: [],
cache: [],
chartDataList: [],
activePopup: null,
},
mixins: [mixinsVue],
created() {
this.data = JSON.parse('{{.Servers}}').servers;
this.group()
},
mounted() {
$('.nezha-secondary-font.info.icon').popup({
popup: '.ui.content.popup',
exclusive: true,
});
},
methods: {
togglePopup(event, id) {
// 切换弹出层的激活状态
this.activePopup = this.activePopup === id ? null : id;
this.showCharts(id);
},
isActive(id) {
// 检查弹出层是否处于激活状态
return this.activePopup === id;
},
closePopup(id) {
this.activePopup = null;
},
showCharts(id) {
// 发起数据请求
const url = `/api/v1/monitor/${id}`;
fetch(url)
.then(response => response.json())
.then(data => {
if (data.result) { // 数据请求成功,更新数据并渲染图表
this.chartDataList[id - 1] = data.result;
this.$nextTick(() => {
this.renderCharts(id);
});
} else {
console.log('this agent (id:'+ id + ') has no monitor.');
}
})
.catch(error => {
console.error('Error fetching data:', error);
});
},
renderCharts(id) {
if (!this.chartDataList[id - 1]) return;
const MaxTCPPingValue = {{.MaxTCPPingValue}} ? {{.MaxTCPPingValue}} : 300;
const isMobile = this.checkIsMobile();
const fontSize = isMobile ? 10 : 9;
const itemGap = isMobile ? 5 : 2;
const itemWidth = isMobile ? 20 : 15;
const gridLeft = 25;
const gridRight = 12;
const fontColor = "rgba(0, 0, 0, 0.68)";
const backgroundColor = '';
const borderColor = "#ffffff";
const chartData = this.chartDataList[id - 1];
const chartContainer = this.$refs[`chart${id}`][0];
const chart = echarts.init(chartContainer, null, {
renderer: 'canvas',
useDirtyRect: false,
width: 'auto',
height: 120,
});
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
const seriesData = chartData.map(item => {
let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => {
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1;
}
return [new Date(item.created_at[index]).toLocaleString(), avgDelay];
});
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
item.monitor_name = item.monitor_name + " " + lossRate + "%";
return {
name: item.monitor_name,
type: 'line',
smooth: true,
symbol: 'none',
data: data
};
});
const option = {
backgroundColor: backgroundColor,
title: {
show: false
},
tooltip: {
show: true,
trigger: 'axis',
textStyle: {
fontSize: fontSize,
color: fontColor
}
},
legend: {
data: chartData.map(item => item.monitor_name),
show: true,
textStyle: {
fontSize: fontSize,
color: fontColor
},
lineStyle: {
cap: 'butt'
},
top: 0,
bottom: 0,
itemGap: itemGap,
itemWidth: itemWidth,
padding: [5,0,5,0]
},
xAxis: {
type: 'time',
data: xAxisData,
axisLabel: {
textStyle: {
fontSize: fontSize
}
}
},
yAxis: {
type: 'value',
axisLabel: {
textStyle: {
fontSize: fontSize
}
}
},
dataZoom: [
{
show: false,
type: 'slider',
start: 0,
end: 100
}
],
series: seriesData,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: '40',
bottom: '20',
left: gridLeft,
right: gridRight
}
};
chart.setOption(option);
},
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
return window.innerWidth <= 768;
},
toFixed2(f) {
return f.toFixed(2)
},
@@ -227,7 +394,7 @@
secondToDate(s) {
var d = Math.floor(s / 3600 / 24);
if (d > 0) {
return d + ' {{tr "Day"}}'
return d + " {{tr "Day"}}"
}
var h = Math.floor(s / 3600 % 24);
var m = Math.floor(s / 60 % 60);
@@ -238,8 +405,48 @@
return new Date(t * 1000).toLocaleString()
},
formatByteSize(bs) {
const x = readableBytes(bs)
const x = this.readableBytes(bs)
return x != "NaN undefined" ? x : '0B'
},
readableBytes(bytes) {
if (!bytes) {
return '0B'
}
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
},
getCoreAndGHz(str){
if((str || []).hasOwnProperty(0) === false){
return '';
}
str = str[0];
let GHz = str.match(/(\d|\.)+GHz/g);
let Core = str.match(/(\d|\.)+ Physical/g);
GHz = GHz!==null?GHz.hasOwnProperty(0)===false?'':GHz[0]:''
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
if(Core === '?'){
let Core = str.match(/(\d|\.)+ Virtual/g);
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
return Core.replace('Virtual','Core')
}
return Core.replace('Physical','Core');
},
getK2Gb(bs){
bs = bs / 1024 /1024 /1024;
if(bs>=1){
return Math.ceil(bs.toFixed(2)) + 'GB';
}else{
bs = bs * 1024;
return Math.ceil(bs.toFixed(2)) + 'MB';
}
},
listTipsMouseenter(obj,strs,tipsNum=1){
this.layerIndex = layer.tips(strs, '#'+obj,{tips: [tipsNum, 'rgb(0 0 0 / 85%)'],time:0});
$('#'+obj).attr('layerIndex',this.layerIndex)
},
listTipsMouseleave(obj){
layer.close(this.layerIndex)
}
}
})
+47
View File
@@ -0,0 +1,47 @@
{{define "theme-default/menu"}}
<div id="app">
<div class="ui large top fixed menu nb-menu" style="z-index:9999999;">
<div class="ui container">
<a class="item" href="/">
<img src="/static/logo.svg?v20210804">
</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="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>
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
</div>
</div>
</template>
<template v-else>
<a href="/service" class='item'><i class="rss icon"></i>{{tr "Services" }}</a>
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
</template>
<div class="item ui simple dropdown">
<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="(value, key) in templates" :key="key" @click="toggleTemplate(key)" class="item"><i class="th large icon"></i>@#value#@
<i class="check icon" v-if="preferredTemplate === key"></i>
</a>
</div>
</div>
{{if .Admin}}
<div class="item right item-right ui simple dropdown">
<div class="text">
<i class="user icon" style="margin-right:3px"></i>{{.Admin.Name}}
<i class="dropdown icon"></i>
</div>
<div class="menu">
<a class="item" href="/server"><i class="terminal icon"></i>{{tr "AdminPanel"}}</a>
<a class="item" @click="logOut({{.Admin.ID}})"><i class="logout icon"></i>{{tr "Logout"}}</a>
</div>
</div>
{{else}}
<a href="/login" class="item right item-right" style="padding-right:1.2rem"><i class="sign-in icon"></i>{{tr "Login"}}</a>
{{end}}
</div>
</div>
{{template "component/confirm" .}}
{{end}}
+8 -5
View File
@@ -1,10 +1,10 @@
{{define "theme-default/network"}}
{{template "common/header" .}}
{{template "theme-default/header" .}}
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
{{template "common/menu" .}}
<div class="nb-container" id="app">
{{template "theme-default/menu" .}}
<div class="nb-container">
<div class="ui container">
<div class="service-status">
<table class="ui celled table">
@@ -22,8 +22,7 @@
</div>
</div>
{{template "common/footer" .}}
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
{{template "theme-default/footer" .}}
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
@@ -36,6 +35,9 @@
el: '#app',
delimiters: ['@#', '#@'],
data: {
page: 'network',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servers: initData,
option: {
tooltip: {
@@ -93,6 +95,7 @@
},
chartOnOff: true,
},
mixins: [mixinsVue],
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
+15 -4
View File
@@ -1,9 +1,9 @@
{{define "theme-default/service"}}
{{template "common/header" .}}
{{template "theme-default/header" .}}
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
{{template "common/menu" .}}
{{template "theme-default/menu" .}}
<div class="nb-container">
<div class="ui container">
<div class="service-status">
@@ -76,10 +76,21 @@
{{end}}
</tbody>
</table>
{{end}}
</div>
</div>
</div>
{{template "common/footer" .}}
{{template "theme-default/footer" .}}
<script>
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
page: 'service',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}}
},
mixins: [mixinsVue]
})
</script>
{{end}}
+14 -1
View File
@@ -1,7 +1,20 @@
{{define "theme-server-status/content-footer"}}
<footer class="container" style="padding-bottom: 2rem;">
<p style="text-align: center; font-size: 10px;">
{{ .Conf.Site.Brand }} | Theme <a target="_blank" href="https://github.com/cppla/ServerStatus">ServerStatus</a> | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
</p>
</footer>
<aside class="toolbox">
<span class="toggleView">
<i v-if="showGroup" @click="toggleView" class="show-nogroup bi bi-justify"></i>
<i v-else @click="toggleView" class="show-group bi bi-view-stacked"></i>
</span>
<span class="setTheme">
<i v-if="theme === 'light'" @click="setTheme('dark', true)" class="setTheme-dark bi bi-moon-fill"></i>
<i v-else @click="setTheme('light', true)" class="setTheme-light bi bi-brightness-high-fill"></i>
</span>
<span v-if="showGoTop" class="showGoTop">
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
</span>
</aside>
{{end}}
+54 -42
View File
@@ -1,49 +1,61 @@
{{define "theme-server-status/content-nav"}}
<div role="navigation" class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container pl-md-unset">
<div class="navbar-header">
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand pl-md-unset">
<img src="/static/logo.svg?v20210804" style="height: 2rem;display: inline-block;">
{{.Conf.Site.Brand}}
</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">{{tr "Home" }}</a></li>
<li><a href="/service">{{tr "Services" }}</a></li>
<li><a href="/network">{{tr "NetworkSpiter" }}</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<header role="navigation" class="navbar navbar-inverse navbar-fixed-top" style="z-index:99999999;">
<div class="container">
<div class="navbar-header">
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">
<img src="/static/logo.svg" style="height: 2rem;display: inline-block;">
{{.Conf.Site.Brand}}
</a>
</div>
<nav id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
<template v-if="isMobile">
<li class="dropdown">
<a data-toggle="dropdown" href="#">{{tr "Menu" }}<b class="caret"></b></a>
<ul class="dropdown-menu">
{{if .Admin}}
<li><a href="/server">{{tr "AdminPanel" }} ({{.Admin.Name}})</a></li>
{{else}}
<li><a href="/login">{{tr "Login" }}</a></li>
{{end}}
<li><a href="#" @click="setSystemTheme">{{tr "FollowSystem" }}
<span style="color: #fff" v-if="isSystemTheme"> ✔️</span></a>
</li>
<li><a href="#" @click="setTheme('dark', true)">{{tr "DarkMode" }}
<span v-if="theme === 'dark' && !isSystemTheme"> ✔️</span></a>
</li>
<li><a href="#" @click="setTheme('light', true)">{{tr "LightMode" }}
<span v-if="theme === 'light' && !isSystemTheme"> ✔️</span></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>
</ul>
</div>
</div>
</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>
<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>
<ul class="dropdown-menu">
<li v-for="(value, key) in templates" :key="key">
<a @click="toggleTemplate(key)">
<i class="list icon"></i>@#value#@
<i class="check icon" v-if="preferredTemplate === key"></i>
</a>
</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{if .Admin}}
<li class="dropdown">
<a data-toggle="dropdown"><i class="user icon"></i>{{.Admin.Name}}<b class="caret"></b></a>
<ul class="dropdown-menu" style="margin-bottom:20px;">
<li><a href="/server"><i class="terminal icon"></i>{{tr "AdminPanel" }}</a></li>
<li><a @click="logOut({{.Admin.ID}})"><i class="logout icon"></i>{{tr "Logout"}}</a></li>
</ul>
</li>
{{else}}
<li><a href="/login"><i class="sign-in icon"></i>{{tr "Login" }}</a></li>
{{end}}
</ul>
</nav>
</div>
</div>
</header>
{{end}}
+11 -11
View File
@@ -6,16 +6,16 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20231207">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240225">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css">
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-logos@0.17/assets/font-logos.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="/static/theme-server-status/js/html5shiv.js"></script>
@@ -24,11 +24,11 @@
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
<script src="/static/theme-server-status/js/jquery.min.js"></script>
<script src="/static/theme-server-status/js/bootstrap.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/theme-server-status/js/mixin.js"></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
<script src="/static/theme-server-status/js/mixin.js?v20240226"></script>
</head>
<body>
{{end}}
{{end}}
@@ -0,0 +1,134 @@
{{define "theme-server-status/home-group-false"}}
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="node-cell status center">{{tr "Status"}}</th>
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
</tr>
</thead>
<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)">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
<div v-else class="status-icon offline"></div>
</div>
</td>
<td class="node-cell name center">@#node.name#@</td>
<td class="node-cell os center">
<i v-if='node.os == "darwin"' class="apple icon"></i>
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#node.os#@</span>
</td>
<td style="text-align: center;" class="node-cell location">
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">@#node.location#@</span>
</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 network">@#node.network#@</td>
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
<td class="node-cell cpu">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
</div>
</div>
</td>
<td class="node-cell memory">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.memory.style" :class="node.memory.class">
<small>@#node.memory.percent#@%</small>
</div>
</div>
</td>
<td class="node-cell hdd">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
</div>
</div>
</td>
</tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID">
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
</span>
<span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span>
@#node.host.CPU.join(",")#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
@#formatByteSize(node.state.DiskUsed)#@ / @#formatByteSize(node.host.DiskTotal)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
</span>
<span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
@#node.state.ProcessCount#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
@#formatTimestamp(node.host.BootTime)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
@#new Date(node.lastActive).toLocaleString()#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Uptime"}}:</span>
@#node.uptime#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
@#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>
</span>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
{{end}}
@@ -0,0 +1,137 @@
{{define "theme-server-status/home-group-true"}}
<table class="table table-striped table-condensed table-hover">
<thead>
<tr class="node-group-tag">
<th colspan="16" style="border:none;">@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@</th>
</tr>
<tr class="node-group-cell">
<th class="node-cell status center">{{tr "Status"}}</th>
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
</tr>
</thead>
<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)">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
<div v-else class="status-icon offline"></div>
</div>
</td>
<td class="node-cell name center">@#node.name#@</td>
<td class="node-cell os center">
<i v-if='node.os == "darwin"' class="apple icon"></i>
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#node.os#@</span>
</td>
<td style="text-align: center;" class="node-cell location">
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span>
</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 network">@#node.network#@</td>
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
<td class="node-cell cpu">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
</div>
</div>
</td>
<td class="node-cell memory">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.memory.style" :class="node.memory.class">
<small>@#node.memory.percent#@%</small>
</div>
</div>
</td>
<td class="node-cell hdd">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
</div>
</div>
</td>
</tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID">
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
</span>
<span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span>
@#node.host.CPU.join(",")#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
@#formatByteSize(node.state.DiskUsed)#@ / @#formatByteSize(node.host.DiskTotal)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
</span>
<span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
@#node.state.ProcessCount#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
@#formatTimestamp(node.host.BootTime)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
@#new Date(node.lastActive).toLocaleString()#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Uptime"}}:</span>
@#node.uptime#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
@#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>
</span>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
{{end}}
+195 -163
View File
@@ -1,153 +1,41 @@
{{define "theme-server-status/home"}}
{{template "theme-server-status/header" .}}
<div id="app">
{{template "theme-server-status/content-nav" .}}
<div class="container table-responsive content" style="max-width: 95vw" v-for="group in nodes">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="node-group-tag" colspan="16" style="border:none;">@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@</th>
</th>
</tr>
<tr>
<th class="node-cell status center">{{tr "Status"}}</th>
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell ram center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
</tr>
</thead>
<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'">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
<div v-else class="status-icon offline"></div>
</div>
</td>
<td class="node-cell name center">@#node.name#@</td>
<td class="node-cell os center">
<i v-if='node.os == "darwin"' class="apple icon"></i>
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#node.os#@</span>
</td>
<td style="text-align: center;" class="node-cell location">
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span>
</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 network">@#node.network#@</td>
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
<td class="node-cell cpu">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
</div>
</div>
</td>
<td class="node-cell memory">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.memory.style" :class="node.memory.class">
<small>@#node.memory.percent#@%</small>
</div>
</div>
</td>
<td class="node-cell hdd">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
</div>
</div>
</td>
</tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID">
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
</span>
<span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span>
@#node.host.CPU.join(",")#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
@#formatByteSize(node.state.DiskUsed)#@ / @#formatByteSize(node.host.DiskTotal)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
</span>
<span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
@#node.state.ProcessCount#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
@#formatTimestamp(node.host.BootTime)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
@#new Date(node.lastActive).toLocaleString()#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
@#node.host.Version#@
</span>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
{{template "theme-server-status/content-nav" .}}
<!-- showGroup true -->
<template v-if="showGroup">
<section class="container table-responsive content" style="max-width: 95vw" v-for="group in nodesTag">
{{template "theme-server-status/home-group-true" .}}
</section>
</template>
<!-- showGroup false -->
<template v-else>
<section class="container table-responsive content" style="max-width: 95vw">
{{template "theme-server-status/home-group-false" .}}
</section>
</template>
{{template "theme-server-status/content-footer" .}}
</div>
<script>
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
nodes: [],
page: 'index',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
nodesTag: [],
nodesNoTag: [],
chartDataList: []
},
mixins: [mixinsVue],
created() {
const initData = JSON.parse('{{.Servers}}').servers;
this.nodes = groupingData(this.handleNodes(initData),"Tag");
this.initTheme()
initData = JSON.parse('{{.Servers}}').servers;
if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(initData),"Tag");
} else {
this.nodesNoTag = this.handleNodes(initData);
}
},
mounted() {
this.connect();
@@ -231,13 +119,18 @@
return x !== "NaN undefined" ? x : '0B'
},
formatPercent(live, used, total) {
const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
return this.formatPercents(percent)
//const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
const percent = (this.toFixed2(used / total * 100) || 0)
return this.formatPercents(live,percent)
},
formatPercents(percent) {
formatPercents(live,percent) {
//if(!live) { percent = 0; }
if (percent <= 0) {
percent = 0;
}
if (percent >= 100) {
percent = 100;
}
if (!this.cache[percent]) {
this.cache[percent] = {
class: 'progress-bar progress-bar-success',
@@ -279,8 +172,12 @@
const lastActive = new Date(ns.LastActive).getTime()
data.servers[i].live = data.now - lastActive <= 10 * 1000;
}
}
this.nodes = groupingData(this.handleNodes(data.servers),"Tag");
}
if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
} else {
this.nodesNoTag = this.handleNodes(data.servers);
}
}
ws.onclose = () => {
setTimeout(function () {
@@ -309,7 +206,7 @@
load: this.toFixed2(server.State.Load1),
network: this.getNetworkSpeed(server.State.NetInSpeed, server.State.NetOutSpeed),
traffic: this.formatByteSize(server.State.NetInTransfer) + ' | ' + this.formatByteSize(server.State.NetOutTransfer),
cpu: this.formatPercents(this.toFixed2(server.State.CPU)),
cpu: this.formatPercents(server.live, this.toFixed2(server.State.CPU)),
memory: this.formatPercent(server.live, server.State.MemUsed, server.Host.MemTotal),
hdd: this.formatPercent(server.live, server.State.DiskUsed, server.Host.DiskTotal),
online: server.live,
@@ -324,29 +221,164 @@
},
getNetworkSpeed(netInSpeed, netOutSpeed) {
return this.formatByteSize(netInSpeed) + ' | ' + this.formatByteSize(netOutSpeed)
},
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;
// 发起数据请求
const url = `/api/v1/monitor/${id}`;
fetch(url)
.then(response => response.json())
.then(data => {
if (data.result) { // 数据请求成功,更新数据并渲染图表
this.chartDataList[id - 1] = data.result;
this.$nextTick(() => {
this.renderCharts(id);
});
} else {
console.log('this agent (id:'+ id + ') has no monitor.');
}
})
.catch(error => {
console.error('Error fetching data:', error);
});
},
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图表的逻辑,
// 第一步,查找已经渲染出的图表容器,并销毁它
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 = {{.MaxTCPPingValue}} ? {{.MaxTCPPingValue}} : 300;
const isMobile = this.checkIsMobile();
const fontSize = isMobile ? 10 : 14;
const gridLeft = isMobile ? 25 : 36;
const gridRight = isMobile ? 5 : 20;
const legendLeft = isMobile ? 'center' : 'center';
const legendTop = isMobile ? 5 : 5;
const legendPadding= isMobile ? [5,0,5,0] : [5,0,5,0];
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
const chartTheme = theme == "dark" ? "dark" : "default";
const fontColor = theme == "dark" ? "#f1f1f1" : "#000000";
const backgroundColor = theme == "dark" ? "#1C1D26" : '';
const tooltipBackgroundColor = theme == "dark" ? "#1C1D26" : '#ffffff';
const tooltipBorderColor = theme == "dark" ? "#31363B" : "#ffffff";
// 渲染图表
const chart = echarts.init(chartContainer, chartTheme, {
renderer: 'canvas',
useDirtyRect: false,
width: 'auto',
height: 300,
});
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
const seriesData = chartData.map(item => {
let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => {
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1;
}
return [new Date(item.created_at[index]).toLocaleString(), avgDelay];
});
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
if (!item.monitor_name.includes("%")) {
item.monitor_name = item.monitor_name + " " + lossRate + "%";
}
return {
name: item.monitor_name,
type: 'line',
smooth: true,
symbol: 'none',
data: data
};
});
const option = {
backgroundColor: backgroundColor,
title: {
show: false
},
tooltip: {
trigger: 'axis',
backgroundColor: tooltipBackgroundColor,
borderColor: tooltipBorderColor,
textStyle: {
fontSize: fontSize,
color: fontColor
}
},
legend: {
data: chartData.map(item => item.monitor_name),
show: true,
textStyle: {
fontSize: fontSize,
color: fontColor
},
top: legendTop,
bottom: 0,
left: legendLeft,
padding: legendPadding
},
xAxis: {
type: 'time',
data: xAxisData,
axisLabel: {
textStyle: {
fontSize: fontSize
}
}
},
yAxis: {
type: 'value',
axisLabel: {
textStyle: {
fontSize: fontSize
}
}
},
dataZoom: [
{
type: 'slider',
start: 0,
end: 100
}
],
series: seriesData,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: '40',
left: gridLeft,
right: gridRight
}
};
chart.setOption(option);
},
reloadCharts() { // 重新加载所有图表
const data = JSON.parse('{{.Servers}}').servers;
data.forEach(node => {
const id = node.ID;
const chartData = this.chartDataList[id - 1];
if (chartData) {
this.renderCharts(id,true);
}
});
}
}
})
function groupingData(data, field) {
let map = new Map();
let dest = [];
data.forEach(item => {
if (!map.has(item[field])) {
dest.push({
[field]: item[field],
data: [item]
});
map.set(item[field], item);
} else {
dest.find(dItem => dItem[field] === item[field]).data.push(item);
}
});
return dest;
}
</script>
{{template "theme-server-status/footer" .}}
{{end}}
+27 -20
View File
@@ -23,12 +23,15 @@
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
MaxTCPPingValue = 1000;
}
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
page: 'network',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servers: initData,
option: {
tooltip: {
@@ -70,7 +73,7 @@
},
dataZoom: [
{
start: 94,
start: 0,
end: 100
}
],
@@ -80,19 +83,16 @@
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%']
boundaryGap: false
},
series: [],
},
chartOnOff: true,
},
mixins: [mixinsVue],
created() {
this.initTheme();
},
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
this.parseMonitorInfo(monitorInfo);
},
methods: {
getFontLogoClass(str) {
@@ -153,15 +153,15 @@
return '';
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
@@ -175,11 +175,13 @@
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = monitorInfo.result[i].avg_delay[j];
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
if (avgDelay > 0) {
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
@@ -189,7 +191,13 @@
type: 'line',
smooth: true,
symbol: 'none',
data: data
data: data,
markPoint: {
data: [
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
]
}
});
}
this.option.title.text = monitorInfo.result[0].server_name;
@@ -214,4 +222,3 @@
</script>
{{template "theme-server-status/footer" .}}
{{end}}
@@ -0,0 +1,46 @@
{{define "theme-server-status/service-group-false"}}
<table class="table table-striped table-condensed table-hover service-status">
<thead>
<tr class="node-group-tag">
<th colspan="16" style="border:none;">
{{tr "ServicesManagement"}}
</th>
</tr>
<tr class="node-group-cell">
<th class="node-cell center service-status-th">{{tr "Status"}}</th>
<th class="node-cell center service-name-th">{{tr "Name"}}</th>
<th class="node-cell center service-details-th">{{tr "Details"}}</th>
<th class="node-cell center service-averagelatency-th">{{tr "AverageLatency"}}</th>
<th class="node-cell center service-30daysonline-th">{{tr "30DaysOnline"}}</th>
</tr>
</thead>
<tbody>
<template v-for="service in servicesNoTag">
<tr>
<td class="node-cell center">
<div class="delay-today">
<i class="delay-today-icon" :class="service.health.className"></i>
<span class="delay-today-text">@#service.health.text#@</span>
</div>
</td>
<td class="node-cell center">@#service.name#@</td>
<td class="node-cell center service-details-td">
<template v-for="(item,index) in service.dayDetail">
<div data-toggle="tooltip" data-placement="top" class="service-day-status-icon" :class="item.className"
:title="item.text">
</div>
</template>
</td>
<td class="node-cell center">@#service.avgDelay#@</td>
<td class="node-cell center">
<div class="progress">
<div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
<small>@#service.totalUpTime.percent#@%</small>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
{{end}}
@@ -0,0 +1,51 @@
{{define "theme-server-status/service-group-true"}}
<table class="table table-striped table-condensed table-hover service-status">
<thead>
<tr class="node-group-tag">
<th colspan="16" style="border:none;">
<span v-if="group.type == 1">HTTP-GET</span>
<span v-if="group.type == 2">ICMP-Ping</span>
<span v-if="group.type == 3">TCP-Ping</span>
</th>
</tr>
<tr class="node-group-cell">
<th class="node-cell center service-status-th">{{tr "Status"}}</th>
<th class="node-cell center service-name-th">{{tr "Name"}}</th>
<th class="node-cell center service-details-th">{{tr "Details"}}</th>
<th class="node-cell center service-averagelatency-th">{{tr "AverageLatency"}}</th>
<th class="node-cell center service-30daysonline-th">{{tr "30DaysOnline"}}</th>
</tr>
</thead>
<tbody>
<template v-for="service in group.data">
<tr>
<td class="node-cell center">
<div class="delay-today">
<i class="delay-today-icon" :class="service.health.className"></i>
<span class="delay-today-text">@#service.health.text#@</span>
</div>
</td>
<td class="node-cell center">@#service.name#@</td>
<td class="node-cell center service-details-td">
<template v-for="(item,index) in service.dayDetail">
<div data-toggle="tooltip" data-placement="top" class="service-day-status-icon" :class="item.className"
:title="item.text">
</div>
</template>
</td>
<td class="node-cell center">@#service.avgDelay#@</td>
<td class="node-cell center">
<div class="progress">
<div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
<small>@#service.totalUpTime.percent#@%</small>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
{{end}}
+63 -82
View File
@@ -2,110 +2,89 @@
{{template "theme-server-status/header" .}}
<div id="app">
{{template "theme-server-status/content-nav" .}}
<div class="container content" style="max-width: 95vw">
<table class="table table-striped table-condensed service-status">
<!-- showGroup true -->
<template v-if="showGroup">
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
{{template "theme-server-status/service-group-true" .}}
</section>
</template>
<!-- showGroup false -->
<template v-else>
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
{{template "theme-server-status/service-group-false" .}}
</section>
</template>
<section class="container content table-responsive" style="max-width: 95vw">
{{if .CycleTransferStats}}
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="node-cell center" style="min-width:60px">{{tr "Status"}}</th>
<th class="node-cell center" style="min-width:50px">{{tr "Name"}}</th>
<th class="node-cell center">{{tr "Details"}}</th>
<th class="node-cell center" style="min-width:80px">{{tr "AverageLatency"}}</th>
<th class="node-cell center" style="min-width:80px">{{tr "30DaysOnline"}}</th>
</tr>
<tr class="node-group-tag">
<th colspan="16" style="border:none;">
{{tr "CycleTransferStats"}}
</th>
</tr>
<tr class="node-group-cell">
<th class="node-cell center">ID</th>
<th class="node-cell center">{{tr "Rules"}}</th>
<th class="node-cell center">{{tr "Server"}}</th>
<th class="node-cell center">{{tr "From"}}</th>
<th class="node-cell center">{{tr "To"}}</th>
<th class="node-cell center">MAX</th>
<th class="node-cell center">MIN</th>
<th class="node-cell center">{{tr "NextCheck"}}</th>
<th class="node-cell center">{{tr "CurrentUsage"}}</th>
<th class="node-cell center">{{tr "Transleft"}}</th>
</tr>
</thead>
<tbody id="servers">
<template v-for="service in services">
<tbody>
{{range $id, $stats := .CycleTransferStats}}
{{range $innerId, $transfer := $stats.Transfer}}
{{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
<tr>
<td class="node-cell center">
<div class="delay-today">
<i class="delay-today" :class="service.health.className"></i>
@#service.health.text#@
</div>
</td>
<td class="node-cell center">@#service.name#@</td>
<td class="node-cell center">
<template v-for="(item,index) in service.dayDetail">
<div class="service-day-status-icon" :class="item.className"
:data-tooltip="item.text">
</div>
</template>
</td>
<td class="node-cell center">@#service.avgDelay#@</td>
<td class="node-cell center">{{$id}}</td>
<td class="node-cell center">{{$stats.Name}}</td>
<td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
<td class="node-cell center">{{$stats.From|tf}}</td>
<td class="node-cell center">{{$stats.To|tf}}</td>
<td class="node-cell center">{{$stats.Max|bf}}</td>
<td class="node-cell center">{{$stats.Min|bf}}</td>
<td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
<td class="node-cell center">{{$transfer|bf}}</td>
<td class="node-cell center">
<div class="progress">
<div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
<small>@#service.totalUpTime.percent#@%</small>
<div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
<small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
</div>
</div>
</td>
</tr>
</template>
{{end}}
{{end}}
</tbody>
</table>
</div>
<div class="container" style="padding:unset;max-width: 95vw">
{{if .CycleTransferStats}}
<h4 style="text-align: center;">{{tr "CycleTransferStats"}}</h4>
<div class="table-responsive content">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th class="node-cell center">ID</th>
<th class="node-cell center">{{tr "Rules"}}</th>
<th class="node-cell center">{{tr "Server"}}</th>
<th class="node-cell center">{{tr "From"}}</th>
<th class="node-cell center">{{tr "To"}}</th>
<th class="node-cell center">MAX</th>
<th class="node-cell center">MIN</th>
<th class="node-cell center">{{tr "NextCheck"}}</th>
<th class="node-cell center">{{tr "CurrentUsage"}}</th>
<th class="node-cell center">{{tr "Transleft"}}</th>
</tr>
</thead>
<tbody>
{{range $id, $stats := .CycleTransferStats}}
{{range $innerId, $transfer := $stats.Transfer}}
{{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
<tr>
<td class="node-cell center">{{$id}}</td>
<td class="node-cell center">{{$stats.Name}}</td>
<td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
<td class="node-cell center">{{$stats.From|tf}}</td>
<td class="node-cell center">{{$stats.To|tf}}</td>
<td class="node-cell center">{{$stats.Max|bf}}</td>
<td class="node-cell center">{{$stats.Min|bf}}</td>
<td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
<td class="node-cell center">{{$transfer|bf}}</td>
<td class="node-cell center">
<div class="progress">
<div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
<small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
</div>
</div>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</section>
{{template "theme-server-status/content-footer" .}}
</div>
<script>
// 初始化 Tooltip
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
</script>
<script>
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
services: []
page: 'service',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servicesTag: [],
servicesNoTag: [],
},
created() {
this.initData()
this.initData();
},
mounted() {
},
@@ -129,6 +108,7 @@
const services = []
{{range $service := .Services}}
services.push({
type: '{{$service.Monitor.Type}}',
name: '{{$service.Monitor.Name}}',
currentUp: parseInt('{{$service.CurrentUp}}'),
currentDown: parseInt('{{$service.CurrentDown}}'),
@@ -147,7 +127,8 @@
service.dayDetail = this.getDayTails(service)
service.totalUpTime = this.getProgressInfo(this.getPercent(service.totalUp, service.totalDown))
}
this.services = services
this.servicesTag = this.groupingData(services,"type");
this.servicesNoTag = services;
},
getPercent(up, down) {
if (!up) {