Compare commits

...
21 Commits
Author SHA1 Message Date
UUBulb 55f5c89c1c feat: description file for custom theme; use gjson (#433)
* feat: description file for custom theme; use gjson

* fix gosec

* remove outdated stuff
2024-10-10 00:08:16 +08:00
nap0o 937696c26d improve: status-server主题日常优化 (#432)
* improve: status-server主题日常优化
1. 首页账单信息:价格设置成“-1”前台展示为”PAYG“,自动续期(autoRenewal)功能增加支持付费周期yr,mo;
2. 当未获取到国旗图标时,默认彩虹旗;
3. 当未获取到os(操作系统)信息时,默认linux
4. 一些其他优化

* 修改表达不准确的语言文件
2024-10-08 11:39:44 +08:00
github-actions[bot] 08aaea8244 update contributors[no ci] 2024-10-06 12:54:13 +00:00
UUBulbandnaiba b886f70e48 chore: l10n: improve translation (#431)
* l10n: improve translation

* refine

---------

Co-authored-by: naiba <[email protected]>
2024-10-06 20:53:55 +08:00
zhucaidanandnaiba 12c6b8a6f6 Update en-US.toml (#429)
* Update en-US.toml

Optimize translation

* refine

---------

Co-authored-by: naiba <[email protected]>
2024-10-06 11:41:19 +08:00
UUBulb ebcc8a6efb installer: fix docker-compose detection logic (#428) 2024-10-06 09:35:12 +08:00
UUBulb c4d8b63d32 installer: fix docker-compose detection (#427) 2024-10-05 23:43:16 +08:00
nap0o 318da135b9 improve: status-server主题agent账单信息展示 (#425)
1. 支持价格和期限单独设置
2. 账单信息附加到group分组控制
3. 一些其他优化
2024-10-04 23:35:44 +08:00
github-actions[bot] da74bf4749 update contributors[no ci] 2024-10-04 15:33:58 +00:00
lyjandnaiba 3250283ef9 fix: 允许更新默认分组 (#426)
* fix: 允许更新默认分组

* improve: check null

---------

Co-authored-by: naiba <[email protected]>
2024-10-04 23:33:40 +08:00
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
nap0o f78ba281fb Default主题fixbug (#421)
* default主题fixbug
1.补上误删的网页title
2.修复network页纵坐标数值显示不全
3.修复network,多个legend导致的遮挡主图表问题

* fix

* fix

* fix

* fix

* fix

* fix
2024-09-07 14:44:30 +08:00
github-actions[bot] 7f9f9a9129 update contributors[no ci] 2024-09-06 15:31:54 +00:00
nap0o b701efd9b5 server-status和default主题:feat & improve & fix (#420)
* feat & improve & fix
1. 增加WebAppManifest文件,实现把哪吒监控网页伪装成app放在移动端桌面
2. vps地图分布图增加数据异步加载loading效果
3. 修复echart图表纵坐标轴因数值过大显示不全bug

* 刷新CDN缓存

* fix footer

* 4.提升service页echarts图表相关体验
5.用设置基准1vh的方法解决footer页脚位置在移动端各种浏览器显示不一致的问题
6.修复section标签的使用位置

* 刷新CDN缓存
2024-09-06 23:31:38 +08:00
UUBulb 511f43784e fix dashboard install script (#419) 2024-09-03 08:54:07 +08:00
53 changed files with 1418 additions and 552 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 ./...
+4 -3
View File
@@ -4,7 +4,7 @@
<br> <br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small> <small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br> <br><br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.18.3-brightgreen?style=for-the-badge&logo=linux"> <img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.19.2-brightgreen?style=for-the-badge&logo=linux">
<br> <br>
<br> <br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p> <p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
@@ -44,8 +44,8 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a> <a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
<a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a> <a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a>
<a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a> <a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></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/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a>
<a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a> <a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a> <a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a>
<a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a> <a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a>
@@ -59,6 +59,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a> <a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a> <a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a> <a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<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/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a> <a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a> <a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
@@ -75,9 +76,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a> <a href="https://github.com/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a>
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a> <a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a> <a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
<a href="https://github.com/lyj0309" title="lyj"><img src="https://avatars.githubusercontent.com/u/50474995?v=4" width="50;" alt="lyj"/></a>
<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/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
<a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a> <a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a> <a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a> <a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
<a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a> <a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a>
+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
} }
+40 -26
View File
@@ -3,10 +3,10 @@ package controller
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -34,28 +34,16 @@ func ServeWeb(port uint) *http.Server {
pprof.Register(r) pprof.Register(r)
} }
r.Use(natGateway) r.Use(natGateway)
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" { tmpl := template.New("").Funcs(funcMap)
r.SetFuncMap(funcMap) var err error
r.Use(mygin.RecordPath) tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
r.Static("/static", "resource/static") if err != nil {
r.LoadHTMLGlob("resource/template/**/*.html") panic(err)
} else {
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil {
panic(err)
}
r.StaticFS("/static", http.FS(staticFs))
} }
r.Static("/static-custom", "resource/static/custom") tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
r.StaticFS("/static", http.FS(resource.StaticFS))
routers(r) routers(r)
page404 := func(c *gin.Context) { page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
@@ -106,14 +94,40 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
if !theme.IsDir() { if !theme.IsDir() {
continue continue
} }
// load templates
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name())) themeDir := theme.Name()
if err != nil { if !strings.HasPrefix(themeDir, "theme-") {
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err) log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
continue continue
} }
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
desc, err := os.ReadFile(filepath.Clean(descPath))
if err != nil {
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
continue
}
themeName, err := utils.GjsonGet(desc, "name")
if err != nil {
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
continue
}
// load templates
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
t, err := ret.ParseGlob(templatePath)
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
continue
}
themeKey := strings.TrimPrefix(themeDir, "theme-")
model.Themes[themeKey] = themeName.String()
ret = t ret = t
} }
return ret return ret
} }
+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"
+3
View File
@@ -21,6 +21,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
github.com/tidwall/gjson v1.18.0
github.com/xanzy/go-gitlab v0.103.0 github.com/xanzy/go-gitlab v0.103.0
golang.org/x/crypto v0.25.0 golang.org/x/crypto v0.25.0
golang.org/x/net v0.27.0 golang.org/x/net v0.27.0
@@ -70,6 +71,8 @@ require (
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
+6
View File
@@ -180,6 +180,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
-1
View File
@@ -23,7 +23,6 @@ var Themes = map[string]string{
"hotaru": "Hotaru", "hotaru": "Hotaru",
"angel-kanade": "AngelKanade", "angel-kanade": "AngelKanade",
"server-status": "ServerStatus", "server-status": "ServerStatus",
"custom": "Custom(local)",
} }
var DashboardThemes = map[string]string{ var DashboardThemes = map[string]string{
+4 -4
View File
@@ -71,8 +71,8 @@ func (ns *NotificationServerBundle) reqBody(message string) (string, error) {
return string(msgBytes)[1 : len(msgBytes)-1] return string(msgBytes)[1 : len(msgBytes)-1]
}), nil }), nil
case NotificationRequestTypeForm: case NotificationRequestTypeForm:
var data map[string]string data, err := utils.GjsonParseStringMap(n.RequestBody)
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil { if err != nil {
return "", err return "", err
} }
params := url.Values{} params := url.Values{}
@@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
if n.RequestHeader == "" { if n.RequestHeader == "" {
return nil return nil
} }
var m map[string]string m, err := utils.GjsonParseStringMap(n.RequestHeader)
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil { if err != nil {
return err return err
} }
for k, v := range m { for k, v := range m {
+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)
+34 -69
View File
@@ -2,6 +2,7 @@ package ddns
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -14,10 +15,13 @@ import (
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones" const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
type ProviderCloudflare struct { type ProviderCloudflare struct {
isIpv4 bool
domainConfig *DomainConfig
secret string secret string
zoneId string zoneId string
ipAddr string
recordId string recordId string
domainConfig *DomainConfig recordType string
} }
type cfReq struct { type cfReq struct {
@@ -28,13 +32,6 @@ type cfReq struct {
Proxied bool `json:"proxied"` Proxied bool `json:"proxied"`
} }
type cfResp struct {
Result []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}
func NewProviderCloudflare(s string) *ProviderCloudflare { func NewProviderCloudflare(s string) *ProviderCloudflare {
return &ProviderCloudflare{ return &ProviderCloudflare{
secret: s, secret: s,
@@ -54,13 +51,19 @@ func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) err
// 当IPv4和IPv6同时成功才算作成功 // 当IPv4和IPv6同时成功才算作成功
if provider.domainConfig.EnableIPv4 { if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil { provider.isIpv4 = true
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err return err
} }
} }
if provider.domainConfig.EnableIpv6 { if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil { provider.isIpv4 = false
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err return err
} }
} }
@@ -68,19 +71,18 @@ func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) err
return nil return nil
} }
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error { func (provider *ProviderCloudflare) addDomainRecord() error {
err := provider.findDNSRecord(isIpv4) err := provider.findDNSRecord()
if err != nil { if err != nil {
if errors.Is(err, utils.ErrGjsonNotFound) {
// 添加 DNS 记录
return provider.createDNSRecord()
}
return fmt.Errorf("查找 DNS 记录时出错: %s", err) return fmt.Errorf("查找 DNS 记录时出错: %s", err)
} }
if provider.recordId == "" { // 更新 DNS 记录
// 添加 DNS 记录 return provider.updateDNSRecord()
return provider.createDNSRecord(isIpv4)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(isIpv4)
}
} }
func (provider *ProviderCloudflare) getZoneID() error { func (provider *ProviderCloudflare) getZoneID() error {
@@ -96,35 +98,22 @@ func (provider *ProviderCloudflare) getZoneID() error {
return err return err
} }
res := &cfResp{} result, err := utils.GjsonGet(body, "result.0.id")
err = utils.Json.Unmarshal(body, res)
if err != nil { if err != nil {
return err return err
} }
result := res.Result provider.zoneId = result.String()
if len(result) > 0 { return nil
provider.zoneId = result[0].ID
return nil
}
return fmt.Errorf("找不到 Zone ID")
} }
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error { func (provider *ProviderCloudflare) findDNSRecord() error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records") de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
du, _ := url.Parse(de) du, _ := url.Parse(de)
q := du.Query() q := du.Query()
q.Set("name", provider.domainConfig.FullDomain) q.Set("name", provider.domainConfig.FullDomain)
q.Set("type", ipType) q.Set("type", provider.recordType)
du.RawQuery = q.Encode() du.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", du.String(), nil) body, err := provider.sendRequest("GET", du.String(), nil)
@@ -132,36 +121,21 @@ func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
return err return err
} }
res := &cfResp{} result, err := utils.GjsonGet(body, "result.0.id")
err = utils.Json.Unmarshal(body, res)
if err != nil { if err != nil {
return err return err
} }
result := res.Result provider.recordId = result.String()
if len(result) > 0 {
provider.recordId = result[0].ID
return nil
}
return nil return nil
} }
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error { func (provider *ProviderCloudflare) createDNSRecord() error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records") de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
data := &cfReq{ data := &cfReq{
Name: provider.domainConfig.FullDomain, Name: provider.domainConfig.FullDomain,
Type: ipType, Type: provider.recordType,
Content: ipAddr, Content: provider.ipAddr,
TTL: 60, TTL: 60,
Proxied: false, Proxied: false,
} }
@@ -171,21 +145,12 @@ func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
return err return err
} }
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error { func (provider *ProviderCloudflare) updateDNSRecord() error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId) de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
data := &cfReq{ data := &cfReq{
Name: provider.domainConfig.FullDomain, Name: provider.domainConfig.FullDomain,
Type: ipType, Type: provider.recordType,
Content: ipAddr, Content: provider.ipAddr,
TTL: 60, TTL: 60,
Proxied: false, Proxied: false,
} }
+7
View File
@@ -20,3 +20,10 @@ func splitDomain(domain string) (prefix string, realDomain string) {
prefix = domain[:len(domain)-len(realDomain)-1] prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain return prefix, realDomain
} }
func getRecordString(isIpv4 bool) string {
if isIpv4 {
return "A"
}
return "AAAA"
}
+39 -59
View File
@@ -5,6 +5,7 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -19,10 +20,14 @@ import (
const te = "https://dnspod.tencentcloudapi.com" const te = "https://dnspod.tencentcloudapi.com"
type ProviderTencentCloud struct { type ProviderTencentCloud struct {
isIpv4 bool
domainConfig *DomainConfig
recordID uint64
recordType string
secretID string secretID string
secretKey string secretKey string
domainConfig *DomainConfig errCode string
resp *tcResp ipAddr string
} }
type tcReq struct { type tcReq struct {
@@ -36,18 +41,6 @@ type tcReq struct {
RecordId uint64 `json:"RecordId,omitempty"` RecordId uint64 `json:"RecordId,omitempty"`
} }
type tcResp struct {
Response struct {
RecordList []struct {
RecordId uint64
Value string
}
Error struct {
Code string
}
}
}
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud { func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
return &ProviderTencentCloud{ return &ProviderTencentCloud{
secretID: id, secretID: id,
@@ -64,13 +57,19 @@ func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) e
// 当IPv4和IPv6同时成功才算作成功 // 当IPv4和IPv6同时成功才算作成功
var err error var err error
if provider.domainConfig.EnableIPv4 { if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil { provider.isIpv4 = true
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err return err
} }
} }
if provider.domainConfig.EnableIpv6 { if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil { provider.isIpv4 = false
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err return err
} }
} }
@@ -78,33 +77,26 @@ func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) e
return err return err
} }
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error { func (provider *ProviderTencentCloud) addDomainRecord() error {
err := provider.findDNSRecord(isIpv4) err := provider.findDNSRecord()
if err != nil { if err != nil {
return fmt.Errorf("查找 DNS 记录时出错: %s", err) return fmt.Errorf("查找 DNS 记录时出错: %s", err)
} }
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录 if provider.errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord(isIpv4) return provider.createDNSRecord()
} else if provider.resp.Response.Error.Code != "" { } else if provider.errCode != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code) return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.errCode)
} }
// 默认情况下更新 DNS 记录 // 默认情况下更新 DNS 记录
return provider.updateDNSRecord(isIpv4) return provider.updateDNSRecord()
} }
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error { func (provider *ProviderTencentCloud) findDNSRecord() error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain) prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{ data := &tcReq{
RecordType: ipType, RecordType: provider.recordType,
Domain: realDomain, Domain: realDomain,
RecordLine: "默认", RecordLine: "默认",
Subdomain: prefix, Subdomain: prefix,
@@ -116,32 +108,29 @@ func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
return err return err
} }
provider.resp = &tcResp{} result, err := utils.GjsonGet(body, "Response.RecordList.0.RecordId")
err = utils.Json.Unmarshal(body, provider.resp)
if err != nil { if err != nil {
if errors.Is(err, utils.ErrGjsonNotFound) {
if errCode, err := utils.GjsonGet(body, "Response.Error.Code"); err == nil {
provider.errCode = errCode.String()
return nil
}
}
return err return err
} }
provider.recordID = result.Uint()
return nil return nil
} }
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error { func (provider *ProviderTencentCloud) createDNSRecord() error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain) prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{ data := &tcReq{
RecordType: ipType, RecordType: provider.recordType,
RecordLine: "默认", RecordLine: "默认",
Domain: realDomain, Domain: realDomain,
SubDomain: prefix, SubDomain: prefix,
Value: ipAddr, Value: provider.ipAddr,
TTL: 600, TTL: 600,
} }
@@ -150,25 +139,16 @@ func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
return err return err
} }
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error { func (provider *ProviderTencentCloud) updateDNSRecord() error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain) prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{ data := &tcReq{
RecordType: ipType, RecordType: provider.recordType,
RecordLine: "默认", RecordLine: "默认",
Domain: realDomain, Domain: realDomain,
SubDomain: prefix, SubDomain: prefix,
Value: ipAddr, Value: provider.ipAddr,
TTL: 600, TTL: 600,
RecordId: provider.resp.Response.RecordList[0].RecordId, RecordId: provider.recordID,
} }
jsonData, _ := utils.Json.Marshal(data) jsonData, _ := utils.Json.Marshal(data)
+36
View File
@@ -0,0 +1,36 @@
package utils
import (
"errors"
"github.com/tidwall/gjson"
)
var (
ErrGjsonNotFound = errors.New("specified path does not exist")
ErrGjsonWrongType = errors.New("wrong type")
)
func GjsonGet(json []byte, path string) (gjson.Result, error) {
result := gjson.GetBytes(json, path)
if !result.Exists() {
return result, ErrGjsonNotFound
}
return result, nil
}
func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
result := gjson.Parse(jsonObject)
if !result.IsObject() {
return nil, ErrGjsonWrongType
}
ret := make(map[string]string)
result.ForEach(func(key, value gjson.Result) bool {
ret[key.String()] = value.String()
return true
})
return ret, nil
}
+33
View File
@@ -0,0 +1,33 @@
package utils
import (
"embed"
"io/fs"
"os"
)
// HybridFS combines embed.FS and os.DirFS.
type HybridFS struct {
embedFS, dir fs.FS
}
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
subFS, err := fs.Sub(embed, subDir)
if err != nil {
return nil, err
}
return &HybridFS{
embedFS: subFS,
dir: os.DirFS(localDir),
}, nil
}
func (hfs *HybridFS) Open(name string) (fs.File, error) {
// Ensure embed files are not replaced
if file, err := hfs.embedFS.Open(name); err == nil {
return file, nil
}
return hfs.dir.Open(name)
}
+50 -32
View File
@@ -23,19 +23,19 @@ other = "Home"
other = "Back to Homepage" other = "Back to Homepage"
[AdminPanel] [AdminPanel]
other = "Admin Panel" other = "Dashboard"
[Logout] [Logout]
other = "Logout" other = "Log out"
[Login] [Login]
other = "Login" other = "Log in"
[ConfirmLogout] [ConfirmLogout]
other = "Do you want to Logout?" other = "Confirm Logging Out?"
[AfterLoggingOutYouHaveToLoginAgain] [AfterLoggingOutYouHaveToLoginAgain]
other = "You need to Login again to continue" other = "You will need to log in again to continue"
[Cancel] [Cancel]
other = "Cancel" other = "Cancel"
@@ -56,7 +56,7 @@ other = "Cron Expression"
other = "Backup" other = "Backup"
[3amDaily] [3amDaily]
other = "(Daily at 3 AM)" other = "(At 3 AM)"
[Command] [Command]
other = "Command" other = "Command"
@@ -71,7 +71,7 @@ other = "Ignore All, Execute Only on Specific Servers"
other = "Cover All, Except Specific Servers" other = "Cover All, Except Specific Servers"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "Execute on Trigger Server" other = "Execute on Alarmed Servers"
[SpecificServers] [SpecificServers]
other = "Specific Servers" other = "Specific Servers"
@@ -95,7 +95,7 @@ other = "Cron Task"
other = "Trigger Task" other = "Trigger Task"
[TheFormaOfTheScheduleIs] [TheFormaOfTheScheduleIs]
other = "The Cron Expression is like:" other = "The Cron Expression format is:"
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails] [SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
other = "Sec Min Hour Day Month Week, see details in " other = "Sec Min Hour Day Month Week, see details in "
@@ -104,7 +104,7 @@ other = "Sec Min Hour Day Month Week, see details in "
other = "CRON Expression Format" other = "CRON Expression Format"
[IntroductionOfCommands] [IntroductionOfCommands]
other = "Introduction of Commands: Writing commands is similar to shell/bat scripts. It's advised not to break lines, and multiple commands can be connected with <code>&&</code> or <code>&</code>. If a command cannot be found, it might be due to <code>PATH</code> environment variable configuration issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or use the command's absolute path." other = "Note on writing commands: It is similar to shell/bat scripts. but it's advised not to start a newline, and connect multiple commands with <code>&&</code> or <code>;</code>. If a command cannot be found, it is possibly due to <code>PATH</code> environment variable issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or just use the absolute path."
[AddMonitor] [AddMonitor]
other = "Add Service Monitor" other = "Add Service Monitor"
@@ -119,7 +119,7 @@ other = "Target"
other = "Type" other = "Type"
[SslExpirationOrChange] [SslExpirationOrChange]
other = "(SSL Expiration or Change)" other = "(Certificate Expiration and Changes)"
[Duration] [Duration]
other = "Interval" other = "Interval"
@@ -134,10 +134,10 @@ other = "Enable Failure Notification"
other = "Failure Notification" other = "Failure Notification"
[MaxLatency] [MaxLatency]
other = "Max Latency (ms)" other = "Maximum Latency (ms)"
[MinLatency] [MinLatency]
other = "Min Latency (ms)" other = "Minimum Latency (ms)"
[EnableLatencyNotification] [EnableLatencyNotification]
other = "Enable Latency Notification" other = "Enable Latency Notification"
@@ -147,7 +147,7 @@ other = "Latency Notification"
[IntroductionOfMonitor] [IntroductionOfMonitor]
other = """ other = """
For type <b>HTTP-GET</b>, enter URL (with http/https, HTTPS protocol will also monitor SSL certificate);<br> For type <b>HTTP-GET</b>, enter URL (with http/https scheme, HTTPS protocol will also monitor SSL certificate);<br>
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br> For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22""" For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
@@ -182,16 +182,16 @@ other = "Notification Trigger Mode"
other = "Always Trigger" other = "Always Trigger"
[ModeOnetimeTrigger] [ModeOnetimeTrigger]
other = "Always Trigger" other = "Trigger Once"
[EnableTriggerTask] [EnableTriggerTask]
other = "Enable Trigger Task" other = "Enable Trigger Task"
[FailTriggerTasks] [FailTriggerTasks]
other = "Tasks triggered during an alarm" other = "Tasks to trigger on an alarm"
[RecoverTriggerTasks] [RecoverTriggerTasks]
other = "Tasks triggered during recovery" other = "Tasks to trigger after recovery"
[Enable] [Enable]
other = "Enable" other = "Enable"
@@ -218,7 +218,7 @@ other = "Einstein Lightspeed 1"
other = "Display Index" other = "Display Index"
[TheLargerTheNumberTheHigherThePriority] [TheLargerTheNumberTheHigherThePriority]
other = "Higher Number, Higher Priority" other = "Greater Number, Higher Priority"
[Secret] [Secret]
other = "Secret" other = "Secret"
@@ -226,11 +226,14 @@ other = "Secret"
[Note] [Note]
other = "Note" other = "Note"
[PublicNote]
other = "Public Note"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux One-Command Install" other = "Linux Installation Command"
[NoDomainAlert] [NoDomainAlert]
other = "Set CDN bypassed Domain/IP in setting before using this function" other = "Please set the IP (or a domain pointed to it) of the original server in settings"
[PushSuccessfully] [PushSuccessfully]
other = "Send Success Notification" other = "Send Success Notification"
@@ -251,13 +254,13 @@ other = "Cover All"
other = "Ignore All" other = "Ignore All"
[ByTrigger] [ByTrigger]
other = "Execute by Trigger" other = "Alarmed Servers"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "Delete Scheduled Task" other = "Delete Scheduled Task"
[ConfirmToDeleteThisScheduledTask] [ConfirmToDeleteThisScheduledTask]
other = "Are you sure you want to Delete?" other = "Confirm deletion?"
[AccessDenied] [AccessDenied]
other = "Access Denied" other = "Access Denied"
@@ -278,22 +281,22 @@ other = "Don't Have an Account?"
other = "HTTP(S)/SSL Certificate" other = "HTTP(S)/SSL Certificate"
[TCPPort] [TCPPort]
other = "TCP Port" other = "TCPing"
[DeleteService] [DeleteService]
other = "Remove Service Monitor" other = "Remove Service Monitor"
[ConfirmToDeleteThisService] [ConfirmToDeleteThisService]
other = "Confirm to Delete This Service?" other = "Confirm Deleting This Service?"
[DeleteNotificationMethod] [DeleteNotificationMethod]
other = "Delete Notification Method" other = "Delete Notification Method"
[ConfirmToDeleteThisNotificationMethod] [ConfirmToDeleteThisNotificationMethod]
other = "Confirm to Delete This Notification Method?" other = "Confirm Deleting This Notification Method?"
[ForceUpdate] [ForceUpdate]
other = "Force to Update" other = "Trigger Agent Update"
[SelectAll] [SelectAll]
other = "Select All" other = "Select All"
@@ -302,7 +305,7 @@ other = "Select All"
other = "Version" other = "Version"
[OneKeyInstall] [OneKeyInstall]
other = "One-Command Install" other = "Installation commands"
[ClickToCopy] [ClickToCopy]
other = "Click To Copy" other = "Click To Copy"
@@ -311,7 +314,7 @@ other = "Click To Copy"
other = "Delete Server" other = "Delete Server"
[ConfirmToDeleteServer] [ConfirmToDeleteServer]
other = "Confirm to Delete Server?" other = "Confirm Deleting This Server?"
[NoServerSelected] [NoServerSelected]
other = "No Server Selected" other = "No Server Selected"
@@ -542,7 +545,7 @@ other = "Scheduled Tasks"
other = "API Management" other = "API Management"
[IssueNewApiToken] [IssueNewApiToken]
other = "Issue New API Token" other = "Generate New API Token"
[Token] [Token]
other = "Token" other = "Token"
@@ -551,10 +554,10 @@ other = "Token"
other = "Delete Token" other = "Delete Token"
[ConfirmToDeleteThisToken] [ConfirmToDeleteThisToken]
other = "Confirm to Delete This Token?" other = "Confirm Deleting This Token?"
[YouAreNotAuthorized] [YouAreNotAuthorized]
other = "Not Authorized for This Page" other = "This Page needs Authorization"
[WrongAccessPassword] [WrongAccessPassword]
other = "Wrong Access Password" other = "Wrong Access Password"
@@ -602,13 +605,13 @@ other = "IP Changed"
other = "Remaining Data" other = "Remaining Data"
[DashboardTheme] [DashboardTheme]
other = "Admin Panel Theme" other = "Dashboard Theme"
[Info] [Info]
other = "Info" other = "Info"
[HideForGuest] [HideForGuest]
other = "Hide for Guest" other = "Hide from Guest"
[Menu] [Menu]
other = "Menu" other = "Menu"
@@ -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"
+22 -4
View File
@@ -71,7 +71,7 @@ other = "Ignorar todo, ejecutar solo a través de servidores específicos"
other = "Incluir todo, excepto servidores específicos" other = "Incluir todo, excepto servidores específicos"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "Ejecutar por servidor disparador" other = "Ejecutar en servidores con alarma"
[SpecificServers] [SpecificServers]
other = "Servidores Específicos" other = "Servidores Específicos"
@@ -104,7 +104,7 @@ other = "Segundos, Minutos, Horas, Días, Meses, Semanas - Ver detalles a"
other = "Formato de Expresión de Cron" other = "Formato de Expresión de Cron"
[IntroductionOfCommands] [IntroductionOfCommands]
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>&</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas." other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>;</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
[AddMonitor] [AddMonitor]
other = "Agregar Monitor de Servicio" other = "Agregar Monitor de Servicio"
@@ -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"
@@ -251,7 +254,7 @@ other = "Cubrir Todo"
other = "Ignorar Todo" other = "Ignorar Todo"
[ByTrigger] [ByTrigger]
other = "Ejecutar por Disparador" other = "Servidores con alarma"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "Eliminar Tarea Programada" other = "Eliminar Tarea Programada"
@@ -278,7 +281,7 @@ other = "¿No tiene una cuenta?"
other = "Certificados HTTP(S)/SSL" other = "Certificados HTTP(S)/SSL"
[TCPPort] [TCPPort]
other = "Puerto TCP" other = "TCPing"
[DeleteService] [DeleteService]
other = "Eliminar Servicio" other = "Eliminar Servicio"
@@ -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"
+23 -5
View File
@@ -71,7 +71,7 @@ other = "忽略所有,仅通过特定服务器执行"
other = "覆盖所有,仅特定服务器不执行" other = "覆盖所有,仅特定服务器不执行"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "由触发的服务器执行" other = "由触发告警的服务器执行"
[SpecificServers] [SpecificServers]
other = "特定服务器" other = "特定服务器"
@@ -104,7 +104,7 @@ other = "秒 分 时 天 月 星期,详情见"
other = "计划表达式格式" other = "计划表达式格式"
[IntroductionOfCommands] [IntroductionOfCommands]
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>&</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。" other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>;</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
[AddMonitor] [AddMonitor]
other = "新增监控" other = "新增监控"
@@ -226,6 +226,9 @@ other = "密钥"
[Note] [Note]
other = "备注" other = "备注"
[PublicNote]
other = "公开备注"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux 一键安装" other = "Linux 一键安装"
@@ -251,7 +254,7 @@ other = "覆盖所有"
other = "忽略所有" other = "忽略所有"
[ByTrigger] [ByTrigger]
other = "触发执行" other = "触发告警的服务器"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "删除计划任务" other = "删除计划任务"
@@ -278,7 +281,7 @@ other = "没有账号?"
other = "HTTP(S)/SSL证书" other = "HTTP(S)/SSL证书"
[TCPPort] [TCPPort]
other = "TCP 端口" other = "TCPing"
[DeleteService] [DeleteService]
other = "移除服务" other = "移除服务"
@@ -422,7 +425,7 @@ other = "活动"
other = "版本" other = "版本"
[NetSpeed] [NetSpeed]
other = "网" other = "网"
[Uptime] [Uptime]
other = "在线" other = "在线"
@@ -687,3 +690,18 @@ other = "取消"
[FMError] [FMError]
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。" other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
[Remaining]
other = "剩余"
[Lifetime]
other = "永续"
[Price]
other = "价格"
[Expired]
other = "已到期"
[Days]
other = "天"
+23 -5
View File
@@ -71,7 +71,7 @@ other = "忽略所有,僅通過特定伺服器執行"
other = "覆蓋所有,僅特定伺服器不執行" other = "覆蓋所有,僅特定伺服器不執行"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "由觸發的伺服器執行" other = "由觸發告警的伺服器執行"
[SpecificServers] [SpecificServers]
other = "特定伺服器" other = "特定伺服器"
@@ -104,7 +104,7 @@ other = "秒 分 時 天 月 星期,詳情見"
other = "排程表達式格式" other = "排程表達式格式"
[IntroductionOfCommands] [IntroductionOfCommands]
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。" other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>;</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
[AddMonitor] [AddMonitor]
other = "新增監控" other = "新增監控"
@@ -226,6 +226,9 @@ other = "金鑰"
[Note] [Note]
other = "備註" other = "備註"
[PublicNote]
other = "公開備註"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux 一鍵安裝" other = "Linux 一鍵安裝"
@@ -251,7 +254,7 @@ other = "覆蓋所有"
other = "忽略所有" other = "忽略所有"
[ByTrigger] [ByTrigger]
other = "觸發執行" other = "觸發告警的伺服器"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "刪除排程任務" other = "刪除排程任務"
@@ -278,7 +281,7 @@ other = "沒有帳號?"
other = "HTTP(S)/SSL證書" other = "HTTP(S)/SSL證書"
[TCPPort] [TCPPort]
other = "TCP 端口" other = "TCPing"
[DeleteService] [DeleteService]
other = "移除服務" other = "移除服務"
@@ -422,7 +425,7 @@ other = "活動"
other = "版本" other = "版本"
[NetSpeed] [NetSpeed]
other = "網" other = "網"
[Uptime] [Uptime]
other = "在線" other = "在線"
@@ -687,3 +690,18 @@ other = "取消"
[FMError] [FMError]
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。" other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
[Remaining]
other = "剩餘"
[Lifetime]
other = "永續"
[Price]
other = "價格"
[Expired]
other = "已到期"
[Days]
other = "天"
+13 -1
View File
@@ -2,10 +2,14 @@ package resource
import ( import (
"embed" "embed"
"github.com/naiba/nezha/pkg/utils"
) )
var StaticFS *utils.HybridFS
//go:embed static //go:embed static
var StaticFS embed.FS var staticFS embed.FS
//go:embed template //go:embed template
var TemplateFS embed.FS var TemplateFS embed.FS
@@ -13,6 +17,14 @@ var TemplateFS embed.FS
//go:embed l10n //go:embed l10n
var I18nFS embed.FS var I18nFS embed.FS
func init() {
var err error
StaticFS, err = utils.NewHybridFS(staticFS, "static", "resource/static/custom")
if err != nil {
panic(err)
}
}
func IsTemplateFileExist(name string) bool { func IsTemplateFileExist(name string) bool {
_, err := TemplateFS.Open(name) _, err := TemplateFS.Open(name)
return err == nil return err == nil
+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", "");
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+36
View File
@@ -0,0 +1,36 @@
{
"name": "Nezha",
"short_name": "Nezha",
"icons": [
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000",
"lang": "eu-US",
"dir": "auto"
}
+36
View File
@@ -0,0 +1,36 @@
{
"name": "哪吒监控",
"short_name": "哪吒监控",
"icons": [
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000",
"lang": "zh-CN",
"dir": "auto"
}
+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;
+19 -14
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);
@@ -27,7 +32,7 @@ body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
body[theme="dark"] .navbar .navbar-nav > .open > a:focus, body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
body[theme="dark"] .navbar .navbar-nav > .open > a:hover, body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
body[theme="dark"] .navbar .navbar-nav > .active > a, body[theme="dark"] .navbar .navbar-nav > .active > a,
body[theme="dark"] .navbar .navbar-nav > .open > a{ body[theme="dark"] .navbar .navbar-nav > .open > a {
background-image: none; background-image: none;
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent;
@@ -53,7 +58,7 @@ body[theme="dark"] .table {
body[theme="dark"] .table > tbody > tr > td:before, body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before, body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before, body[theme="dark"] .table > thead > tr > td:before,
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{ body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
background-color: rgba(58, 62, 65, 1); background-color: rgba(58, 62, 65, 1);
} }
@@ -69,7 +74,7 @@ body[theme="dark"] .table-striped tbody > tr.odd > th {
background-color: rgba(34, 35, 46, 1); background-color: rgba(34, 35, 46, 1);
} }
body[theme="dark"] .table-hover > tbody > tr:hover{ body[theme="dark"] .table-hover > tbody > tr:hover {
background-color: unset; background-color: unset;
} }
@@ -78,18 +83,18 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
} }
/* expandRow展开部分样式 */ /* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{ body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
background-color: rgba(34, 35, 46, 1); background-color: rgba(34, 35, 46, 1);
} }
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{ body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
background-color: rgba(28, 29, 38, 1); background-color: rgba(28, 29, 38, 1);
} }
/* 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 {
@@ -150,29 +155,29 @@ body[theme="dark"] footer p a:hover {
text-decoration: none; text-decoration: none;
} }
body[theme="dark"] .modal-content{ body[theme="dark"] .modal-content {
background-color: rgba(28, 29, 38, 1); background-color: rgba(28, 29, 38, 1);
color: #f1f1f1; color: #f1f1f1;
} }
body[theme="dark"] #earthChartBox .modal-content{ body[theme="dark"] #earthChartBox .modal-content {
background-color: rgba(0, 0, 0, 1); background-color: rgba(0, 0, 0, 1);
color: #f1f1f1; color: #f1f1f1;
} }
body[theme="dark"] .modal-header{ body[theme="dark"] .modal-header {
border-bottom-color:#080808; border-bottom-color:#080808;
} }
body[theme="dark"] .modal-header i.xclose{ body[theme="dark"] .modal-header i.xclose {
color: #f1f1f1; color: #f1f1f1;
} }
body[theme="dark"] .toolbox i{ body[theme="dark"] .toolbox i {
color: rgba(241, 241, 241, 1); color: rgba(241, 241, 241, 1);
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
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);
} }
+3 -3
View File
@@ -86,7 +86,7 @@ body[theme="dark"] .table > thead > tr.node-group-tag > th:before {
body[theme="dark"] .table > tbody > tr > td:before, body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before, body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before, body[theme="dark"] .table > thead > tr > td:before,
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{ body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
/** border-bottom 颜色 **/ /** border-bottom 颜色 **/
background-color: rgba(155, 155, 155, 0.1); background-color: rgba(155, 155, 155, 0.1);
} }
@@ -96,11 +96,11 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
} }
/* expandRow展开部分样式 */ /* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{ body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
background-color: unset; background-color: unset;
} }
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{ body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
background-color: unset; background-color: unset;
} }
/* expandRow展开部分样式结束 */ /* expandRow展开部分样式结束 */
+19 -14
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,
@@ -42,16 +47,16 @@ body[theme="light"] .navbar .navbar-nav .open .dropdown-menu > li > a {
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
body[theme="light"] .navbar .navbar-toggle{ body[theme="light"] .navbar .navbar-toggle {
border:none; border:none;
padding:9px 12px; padding:9px 12px;
} }
body[theme="light"] .navbar .navbar-collapse{ body[theme="light"] .navbar .navbar-collapse {
border-color:rgba(0, 0, 0, 0.085); border-color:rgba(0, 0, 0, 0.085);
} }
body[theme="light"] .navbar .navbar-toggle .icon-bar{ body[theme="light"] .navbar .navbar-toggle .icon-bar {
background-color: rgba(0, 0, 0, 0.87); background-color: rgba(0, 0, 0, 0.87);
} }
@@ -90,12 +95,12 @@ body[theme="light"] .table-striped tbody > tr.odd > th {
body[theme="light"] .table > tbody > tr > td:before, body[theme="light"] .table > tbody > tr > td:before,
body[theme="light"] .table > tfoot > tr > td:before, body[theme="light"] .table > tfoot > tr > td:before,
body[theme="light"] .table > thead > tr > td:before, body[theme="light"] .table > thead > tr > td:before,
body[theme="light"] .table > thead > tr.node-group-cell > th:before{ body[theme="light"] .table > thead > tr.node-group-cell > th:before {
background-color: rgba(221, 221, 221, 1); background-color: rgba(221, 221, 221, 1);
} }
body[theme="light"] .table > tbody > tr.expandRow > td:before, body[theme="light"] .table > tbody > tr.expandRow > td:before,
body[theme="light"] .table > thead > tr.node-group-tag > th:before{ body[theme="light"] .table > thead > tr.node-group-tag > th:before {
background-color: rgba(255, 255, 255, 1); background-color: rgba(255, 255, 255, 1);
} }
@@ -169,36 +174,36 @@ body[theme="light"] footer p a:hover {
text-decoration: none; text-decoration: none;
} }
body[theme="light"] .toolbox i{ body[theme="light"] .toolbox i {
color: rgba(94, 114, 228, .9); color: rgba(94, 114, 228, .9);
background-color: rgba(255, 255, 255, .5); background-color: rgba(255, 255, 255, .5);
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important; box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important;
} }
body[theme="light"] .toolbox .mytoggle .tooltip-inner{ body[theme="light"] .toolbox .mytoggle .tooltip-inner {
background: #ffffff; background: #ffffff;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important; box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important;
} }
body[theme="light"] .toolbox .mytoggle li:hover{ body[theme="light"] .toolbox .mytoggle li:hover {
background-color:#e6e6e6; background-color:#e6e6e6;
} }
body[theme="light"] .modal-content{ body[theme="light"] .modal-content {
background-color: rgba(249, 249, 249, 1); background-color: rgba(249, 249, 249, 1);
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
body[theme="light"] .modal-header{ body[theme="light"] .modal-header {
border-bottom-color:#f1f1f1; border-bottom-color:#f1f1f1;
} }
body[theme="light"] .modal-header i.xclose{ body[theme="light"] .modal-header i.xclose {
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
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) {
+7 -7
View File
@@ -36,16 +36,16 @@ body[theme="light"] .navbar .dropdown-menu {
body[theme="light"] .navbar .dropdown-menu > li > a:focus, body[theme="light"] .navbar .dropdown-menu > li > a:focus,
body[theme="light"] .navbar .dropdown-menu > li > a:hover{ body[theme="light"] .navbar .dropdown-menu > li > a:hover {
/** 二级导航鼠标悬停选中背景 **/ /** 二级导航鼠标悬停选中背景 **/
background-color: rgba(0, 0, 0, .05); background-color: rgba(0, 0, 0, .05);
} }
body[theme="light"] .navbar .navbar-collapse{ body[theme="light"] .navbar .navbar-collapse {
border-color:rgba(0, 0, 0, 0.085); border-color:rgba(0, 0, 0, 0.085);
} }
body[theme="light"] .navbar .navbar-toggle .icon-bar{ body[theme="light"] .navbar .navbar-toggle .icon-bar {
background-color: rgba(0, 0, 0, 0.9); background-color: rgba(0, 0, 0, 0.9);
} }
@@ -87,7 +87,7 @@ body[theme="light"] .table > thead > tr.node-group-tag > th:before {
body[theme="light"] .table > tbody > tr > td:before, body[theme="light"] .table > tbody > tr > td:before,
body[theme="light"] .table > tfoot > tr > td:before, body[theme="light"] .table > tfoot > tr > td:before,
body[theme="light"] .table > thead > tr > td:before, body[theme="light"] .table > thead > tr > td:before,
body[theme="light"] .table > thead > tr.node-group-cell > th:before{ body[theme="light"] .table > thead > tr.node-group-cell > th:before {
/** border-bottom 颜色 **/ /** border-bottom 颜色 **/
background-color: rgba(0, 0, 0, 0.075); background-color: rgba(0, 0, 0, 0.075);
} }
@@ -97,11 +97,11 @@ body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
} }
/* expandRow展开部分样式 */ /* expandRow展开部分样式 */
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before{ body[theme="light"] .table > tbody > tr.expandRow.odd > td:before {
background-color: unset; background-color: unset;
} }
body[theme="light"] .table > tbody > tr.expandRow.even > td:before{ body[theme="light"] .table > tbody > tr.expandRow.even > td:before {
background-color: unset; background-color: unset;
} }
/* expandRow展开部分样式结束 */ /* expandRow展开部分样式结束 */
@@ -123,7 +123,7 @@ body[theme="light"] .progress-bar-danger {
background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%); background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%);
} }
body[theme="light"] .toolbox i{ body[theme="light"] .toolbox i {
background-color: rgba(255, 255, 255, .55); background-color: rgba(255, 255, 255, .55);
} }
+72 -37
View File
@@ -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;
} }
@@ -322,15 +340,17 @@ td.ping-network-quality {
/* 服务页 正文结束 */ /* 服务页 正文结束 */
/* 网络页 正文*/ /* 网络页 正文*/
.network-box .btn-group.open .dropdown-toggle{ .network-box .btn-group.open .dropdown-toggle {
box-shadow:unset; box-shadow:unset;
} }
.network-box .network-box-header{ .network-box .network-box-header {
font-size: 18px; font-size: 18px;
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 {
@@ -345,12 +365,12 @@ td.ping-network-quality {
display: none; display: none;
} }
.network-box .network-box-header .dropdown-menu li{ .network-box .network-box-header .dropdown-menu li {
height: 2em; height: 2em;
cursor: pointer; cursor: pointer;
} }
.network-box .network-box-header .dropdown-menu li.input-group{ .network-box .network-box-header .dropdown-menu li.input-group {
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
position: sticky; position: sticky;
@@ -358,7 +378,7 @@ td.ping-network-quality {
z-index: 99999997; z-index: 99999997;
} }
.network-box .network-box-header .dropdown-menu li.input-group input{ .network-box .network-box-header .dropdown-menu li.input-group input {
width: 100%; width: 100%;
height: 2em; height: 2em;
border-top: none; border-top: none;
@@ -381,12 +401,13 @@ 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;
} }
.network-box .chartTitle i.chartCountryCode{ .network-box .chartTitle i.chartCountryCode {
font-size: 16px; font-size: 16px;
border-radius: 12.5%; border-radius: 12.5%;
} }
@@ -394,17 +415,17 @@ td.ping-network-quality {
/* 网络页 正文结束 */ /* 网络页 正文结束 */
/* 地图版服务器分布图 */ /* 地图版服务器分布图 */
#mapChartBox{ #mapChartBox {
z-index: 999999999; z-index: 999999999;
padding-left: 0 !important; padding-left: 0 !important;
} }
.modal-open .modal{ .modal-open .modal {
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden; overflow-y: hidden;
} }
.modal-dialog-centered{ .modal-dialog-centered {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -414,7 +435,7 @@ td.ping-network-quality {
min-height: calc(100%) /* 确保模态框占据整个视口高度 */ min-height: calc(100%) /* 确保模态框占据整个视口高度 */
} }
.modal-content{ .modal-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: auto; pointer-events: auto;
@@ -422,15 +443,15 @@ td.ping-network-quality {
border: none; border: none;
} }
.modal-header{ .modal-header {
position: relative; position: relative;
} }
.modal-header h4{ .modal-header h4 {
cursor: pointer; cursor: pointer;
} }
.modal-header i.xclose{ .modal-header i.xclose {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
font-size: 30px; font-size: 30px;
@@ -438,32 +459,32 @@ td.ping-network-quality {
right: 10px; right: 10px;
} }
.modal-body{ .modal-body {
padding: 0px; padding: 0px;
} }
a.template-item, a.cdn-item{ a.template-item, a.cdn-item {
display: block; display: block;
} }
footer{ footer {
padding-bottom: 20px; padding-bottom: 20px;
} }
footer p{ footer p {
text-align: center; text-align: center;
font-size: 10px; font-size: 10px;
} }
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
.accordian-body{ .accordian-body {
margin: 5px 0px 5px 10px; margin: 5px 0px 5px 10px;
} }
table.monitor-detail{ table.monitor-detail {
margin:0px 0px 2px 0px; margin:0px 0px 2px 0px;
} }
.table .network-thead-tr th, .table .network-thead-tr th,
.table .node-group-tag th{ .table .node-group-tag th {
font-size:16px; font-size:16px;
padding-bottom:6px; padding-bottom:6px;
} }
@@ -480,16 +501,21 @@ footer p{
padding-top:60px !important; padding-top:60px !important;
} }
.nezha { .nezha {
min-height: calc(74.5vh); min-height: calc(100vh - 90px);
min-height: calc(var(--vh, 1vh) * 100 - 90px);
}
#chartbox {
min-height: calc(100vh - 170px);
min-height: calc(var(--vh, 1vh) * 100 - 170px);
} }
.content { .content {
padding: 0; padding: 0;
margin-bottom: 10px; margin-bottom: 10px;
} }
footer{ footer {
padding-bottom: 10px; padding-bottom: 10px;
} }
.cycle-transfer-stats .dataTables_wrapper{ .cycle-transfer-stats .dataTables_wrapper {
margin-bottom: -10px; margin-bottom: -10px;
} }
.navbar .navbar-nav .open .dropdown-menu>li>a { .navbar .navbar-nav .open .dropdown-menu>li>a {
@@ -511,50 +537,58 @@ footer p{
} }
.node-cell.os, .node-cell.os,
.node-cell.uptime, .node-cell.uptime,
.node-cell.traffic{ .node-cell.traffic {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.node-cell.location{ .node-cell.location {
display: table-cell; display: table-cell;
visibility: visible; visibility: visible;
} }
.node-cell-os-text,.node-cell-location-text{ .node-cell-os-text,.node-cell-location-text {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.accordian-body{ .node-cell.network {
min-width: 95px;
max-width: 95px;
}
.node-cell.remaining {
min-width: 75px;
max-width: 75px;
}
.accordian-body {
margin: 5px 0px 5px 10px; margin: 5px 0px 5px 10px;
} }
table.monitor-detail{ table.monitor-detail {
margin:0px 0px 2px 0px; margin:0px 0px 2px 0px;
} }
.table .network-thead-tr th, .table .network-thead-tr th,
.table .node-group-tag th{ .table .node-group-tag th {
font-size:14px; font-size:14px;
padding-bottom:6px; padding-bottom:6px;
} }
.table tr.network td{ .table tr.network td {
font-size: 12px; font-size: 12px;
} }
.service-status .service-status-th{ .service-status .service-status-th {
min-width:30px; min-width:30px;
} }
.service-status .delay-today{ .service-status .delay-today {
margin-top:4px; margin-top:4px;
justify-content: center; justify-content: center;
} }
.service-status .delay-today i{ .service-status .delay-today i {
margin-right:0px; margin-right:0px;
} }
.service-status .delay-today-text{ .service-status .delay-today-text {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.service-status .service-averagelatency-th{ .service-status .service-averagelatency-th {
min-width:70px; min-width:70px;
} }
.service-status .service-30daysonline-th{ .service-status .service-30daysonline-th {
min-width:75px; min-width:75px;
} }
.toolbox { .toolbox {
@@ -566,12 +600,13 @@ 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;
margin: 10px 0px 10px 0px; margin: 10px 0px 10px 0px;
} }
.network-box .chartTitle i.chartCountryCode{ .network-box .chartTitle i.chartCountryCode {
font-size: 15px; font-size: 15px;
border-radius: 12.5%; border-radius: 12.5%;
} }
+11
View File
@@ -26,7 +26,9 @@ const mixinsVue = {
this.semiTransparent = this.initSemiTransparent(); this.semiTransparent = this.initSemiTransparent();
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;
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight; this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
this.setBenchmarkHeight();
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.setBenchmarkHeight());
}, },
destroyed() { destroyed() {
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('scroll', this.handleScroll);
@@ -152,6 +154,15 @@ 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() {
let vh = window.innerHeight * 0.01;
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">
+14 -10
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"
@@ -155,8 +159,8 @@
} }
function batchEditServerGroup() { function batchEditServerGroup() {
let groupName = prompt('{{tr "InputServerGroupName"}}') let groupName = prompt('{{tr "InputServerGroupName"}}')
if (!groupName) { if (groupName === null) {
return; return
} }
const servers = [] const servers = []
checkBoxList.forEach(cb => { checkBoxList.forEach(cb => {
+14 -5
View File
@@ -3,12 +3,21 @@
<html lang="{{.Conf.Language}}"> <html lang="{{.Conf.Language}}">
<head> <head>
<meta charset="UTF-8"> <title>{{ .Title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection"> <meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#000000" />
{{ if eq .Conf.Language "zh-CN" }}
<link rel="manifest" href="/static/manifest-zh-CN.json?v20240905" />
{{ else }}
<link rel="manifest" href="/static/manifest-en-US.json?v20240905" />
{{ end }}
<link rel="apple-touch-startup-image" href="/static/logo.svg" />
<link rel="shortcut icon" type="image/png" href="/static/logo.svg">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/semantic.min.css"> <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/semantic.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css"> <link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
@@ -21,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>
+1 -1
View File
@@ -194,7 +194,7 @@
] ]
}, },
mixins: [mixinsVue], mixins: [mixinsVue],
created() { created() {
this.servers = JSON.parse('{{.Servers}}').servers; this.servers = JSON.parse('{{.Servers}}').servers;
this.group() this.group()
}, },
+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">
+22 -14
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>
@@ -36,7 +36,11 @@
defaultTemplate: {{.Conf.Site.Theme}}, defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}}, templates: {{.Themes}},
servers: initData, servers: initData,
option: { option: {}
},
mixins: [mixinsVue],
created() {
this.option = {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
position: function (pt) { position: function (pt) {
@@ -69,17 +73,9 @@
right: this.isMobile ? '8%' : '3.8%', right: this.isMobile ? '8%' : '3.8%',
}, },
backgroundColor: '', backgroundColor: '',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [ dataZoom: [
{ {
type: 'slider',
start: 0, start: 0,
end: 100 end: 100
} }
@@ -93,10 +89,8 @@
boundaryGap: false boundaryGap: false
}, },
series: [], series: [],
}, }
chartOnOff: true,
}, },
mixins: [mixinsVue],
mounted() { mounted() {
this.renderChart(); this.renderChart();
this.parseMonitorInfo(monitorInfo); this.parseMonitorInfo(monitorInfo);
@@ -236,6 +230,20 @@
this.option.title.text = monitorInfo.result[0].server_name; this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries; this.option.series = tSeries;
this.option.legend.data = tLegendData; this.option.legend.data = tLegendData;
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 2;
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
const autoIncrement = Math.floor((tLegendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 28 : 34);
const height = 520 + autoIncrement;
const gridTop = 60 + autoIncrement;
this.option.grid = {
left: this.isMobile ? '8%' : '3.8%',
right: this.isMobile ? '8%' : '3.8%',
top: gridTop
};
this.myChart.resize({
width: 'auto',
height: height
});
this.myChart.clear(); this.myChart.clear();
this.myChart.setOption(this.option); this.myChart.setOption(this.option);
}, },
+3 -3
View File
@@ -1,5 +1,5 @@
{{define "theme-server-status/footer"}} {{define "theme-server-status/footer"}}
</div> </section>
<footer class="container-fluid"> <footer class="container-fluid">
<p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p> <p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p>
</footer> </footer>
@@ -28,8 +28,8 @@
</span> </span>
</aside> </aside>
<template v-if="semiTransparent"> <template v-if="semiTransparent">
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20240807"> <link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20241008">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807"> <link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20241008">
</template> </template>
</div> </div>
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}} {{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
+17 -7
View File
@@ -2,11 +2,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{.Conf.Language}}"> <html lang="{{.Conf.Language}}">
<head> <head>
<title>{{ .Title }}</title> <title>{{.Title}}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#000000" />
{{if eq .Conf.Language "zh-CN"}}
<link rel="manifest" href="/static/manifest-zh-CN.json?v20240905" />
{{else}}
<link rel="manifest" href="/static/manifest-en-US.json?v20240905" />
{{end}}
<link rel="apple-touch-startup-image" href="/static/logo.svg" />
<link rel="shortcut icon" type="image/png" href="/static/logo.svg">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css"> <link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css">
@@ -16,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?v202408016"> <link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20241008">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011"> <link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20241008">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811"> <link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20241008">
<script src="/static/theme-server-status/js/mixin.js?v20240811"></script> <script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
+32 -8
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="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.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="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.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>
@@ -30,11 +32,18 @@
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i> <i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i> <i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i> <i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span> <span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
</td> </td>
<td style="text-align: center;" class="node-cell location"> <td class="node-cell location center">
<i :class="'fi fi-' + node.location"></i> <i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'rb') : '')"></i>
<span class="node-cell-location-text text-uppercase">@#node.location#@</span> <span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'RB') : ''#@</span>
</td>
<td v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
<template v-if="node.additional && node.additional.price">
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</template>
</td> </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>
@@ -59,6 +68,21 @@
</div> </div>
</div> </div>
</td> </td>
<td v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="node.additional && node.additional.remaining.format">
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
<div class="additional">
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#node.additional.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">
@@ -66,8 +90,8 @@
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw"> <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">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span> <span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@ <span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@] <span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
</span> </span>
<span class="node-cell-expand" v-if="node.host.CPU"> <span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span> <span class="node-cell-expand-label">CPU:</span>
@@ -143,7 +167,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>
+32 -8
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="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.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="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.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>
@@ -33,11 +35,18 @@
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i> <i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i> <i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i> <i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span> <span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
</td> </td>
<td style="text-align: center;" class="node-cell location"> <td class="node-cell location center">
<i :class="'fi fi-' + node.location"></i> <i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'rb') : '')"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span> <span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'RB') : ''#@</span>
</td>
<td v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
<template v-if="node.additional && node.additional.price">
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</template>
</td> </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>
@@ -62,6 +71,21 @@
</div> </div>
</div> </div>
</td> </td>
<td v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="node.additional && node.additional.remaining.format">
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
<div class="additional">
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#node.additional.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">
@@ -69,8 +93,8 @@
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw"> <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">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span> <span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@ <span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@] <span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
</span> </span>
<span class="node-cell-expand" v-if="node.host.CPU"> <span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span> <span class="node-cell-expand-label">CPU:</span>
@@ -146,7 +170,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>
+588 -97
View File
@@ -3,15 +3,15 @@
{{template "theme-server-status/menu" .}} {{template "theme-server-status/menu" .}}
<!-- showGroup true --> <!-- showGroup true -->
<template v-if="showGroup"> <template v-if="showGroup">
<section class="container-fluid table-responsive content" v-for="group in nodesTag"> <div class="container-fluid table-responsive content" v-for="group in nodesTag">
{{template "theme-server-status/home-group-true" .}} {{template "theme-server-status/home-group-true" .}}
</section> </div>
</template> </template>
<!-- showGroup false --> <!-- showGroup false -->
<template v-else> <template v-else>
<section class="container-fluid table-responsive content"> <div class="container-fluid table-responsive content">
{{template "theme-server-status/home-group-false" .}} {{template "theme-server-status/home-group-false" .}}
</section> </div>
</template> </template>
<div class="modal fade" id="mapChartBox" tabindex="-1" role="dialog" aria-labelledby="mapChartTitle" aria-hidden="true" style="padding-left:0"> <div class="modal fade" id="mapChartBox" tabindex="-1" role="dialog" aria-labelledby="mapChartTitle" aria-hidden="true" style="padding-left:0">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
@@ -42,6 +42,7 @@
servers: [], servers: [],
nodesTag: [], nodesTag: [],
nodesNoTag: [], nodesNoTag: [],
additional: {},
chartDataList: [], chartDataList: [],
ws: null, ws: null,
language: {{.Conf.Language}}, language: {{.Conf.Language}},
@@ -71,17 +72,32 @@
mixins: [mixinsVue], mixins: [mixinsVue],
created() { created() {
this.servers = JSON.parse('{{.Servers}}').servers; this.servers = JSON.parse('{{.Servers}}').servers;
this.additional = this.initAdditional(this.servers);
if(this.showGroup) { if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag"); this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
this.nodesTag.forEach(group => {
group.data.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
});
} else { } else {
this.nodesNoTag = this.handleNodes(this.servers); this.nodesNoTag = this.handleNodes(this.servers);
this.nodesNoTag.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
} }
this.initCountryMap().then(countryMap => { this.initCountryMap().then(countryMap => {
this.countryMap = countryMap; this.countryMap = countryMap;
this.countryNameMap = this.initCountryNameMap(); this.countryNameMap = this.initCountryNameMap();
this.countryServer = this.initCountryServer(); this.countryServer = this.initCountryServer();
this.countryMapChartData = this.initCountryMapChartData(); this.countryMapChartData = this.initCountryMapChartData();
}) });
}, },
mounted() { mounted() {
// 初始化时建立WebSocket连接 // 初始化时建立WebSocket连接
@@ -98,6 +114,49 @@
}); });
}, },
methods: { methods: {
initAdditional(servers) {
let nodes = {};
servers?.forEach(server => {
if (server.PublicNote) {
const remainingFormat = this.getRemainingFormat(server.live, server.PublicNote);
const remainingDays = this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote);
const remainingPercent = this.getRemainingPercent(
this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"),
this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),
server.PublicNote
);
const priceAmount = this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount");
const priceCycle = this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle");
// 初始化节点
nodes[server.ID] = {
"remaining": {},
"price": {}
};
if (remainingFormat) {
nodes[server.ID].remaining.format = remainingFormat;
}
if (remainingDays) {
nodes[server.ID].remaining.days = remainingDays;
}
if (remainingPercent) {
nodes[server.ID].remaining.percent = this.toFixed2(100 - remainingPercent);
}
if (priceAmount) {
nodes[server.ID].price.amount = priceAmount;
}
if (priceCycle && priceAmount) {
nodes[server.ID].price.cycle = priceCycle;
}
}
});
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())
@@ -162,7 +221,10 @@
const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff'; const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff';
const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff"; const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff";
const fontSize = this.isMobile ? 10 : 12; const fontSize = this.isMobile ? 10 : 12;
const fontColor = this.theme == "dark" ? "#000000" : "#000000"; const fontColor = this.theme == "dark" ? "#000000" : "#000000";
const showLoadingMaskColor = this.theme == "dark" ? 'rgba(28, 29, 38, 1)' : 'rgba(249, 249, 249, 1)';
const showLoadingTextColor = this.theme == "dark" ? 'rgba(241, 241, 241, 1)' : 'rgba(0, 0, 0, 1)';
const showLoadingColor = inRangeColor;
const chartContainer = document.getElementById('mapChart'); const chartContainer = document.getElementById('mapChart');
const mapChart = echarts.init(chartContainer, '', { // init图表 const mapChart = echarts.init(chartContainer, '', { // init图表
renderer: 'canvas', renderer: 'canvas',
@@ -170,6 +232,13 @@
width: width, width: width,
height: height, height: height,
}); });
mapChart.showLoading({
text: 'loading',
textColor: showLoadingTextColor,
color: showLoadingColor,
maskColor: showLoadingMaskColor,
zlevel: 2
});
fetch(this.staticUrl + '/maps/' + this.countryMapGeoFile) fetch(this.staticUrl + '/maps/' + this.countryMapGeoFile)
.then(response => response.json()) .then(response => response.json())
.then(worldMap => { .then(worldMap => {
@@ -224,7 +293,10 @@
} }
] ]
}; };
mapChart.setOption(option); setTimeout(() => {
mapChart.hideLoading();
mapChart.setOption(option);
}, 1000);
}); });
}, },
isWindowsPlatform(str) { isWindowsPlatform(str) {
@@ -382,8 +454,22 @@
} }
if(this.showGroup) { if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag"); this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
this.nodesTag.forEach(group => {
group.data.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
});
} else { } else {
this.nodesNoTag = this.handleNodes(data.servers); this.nodesNoTag = this.handleNodes(data.servers);
this.nodesNoTag.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
} }
} }
this.ws.onclose = () => { this.ws.onclose = () => {
@@ -470,101 +556,144 @@
// 如果所有元素的 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 => {
console.error('Error fetching data:', error); console.log('Error fetching data:', error);
}); });
}, },
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 = 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;
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0]; const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode; const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
const chartTheme = theme == "dark" ? "dark" : "default"; const chartTheme = this.theme == "dark" ? "dark" : "default";
const fontColor = theme == "dark" ? "#f1f1f1" : "#000000"; const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
const backgroundColor = theme == "dark" ? '' : ''; const backgroundColor = theme == "dark" ? '' : '';
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)"); const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)"); const 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' : "";
@@ -574,39 +703,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
@@ -626,34 +740,411 @@
} }
} }
}, },
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 'mo':
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 'yr':
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.log("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.log("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') {
return null; // 如果无效,返回 null 或其他错误处理逻辑
}
// 处理特殊时间格式
if (startDate.includes('0000-00-00') || endDate.includes("0000-00-00")) {
return this.formatPercents(online, this.toFixed2(100));
}
// 检查 startDate 和 endDate 是否为合法的Date
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
return {
"class": "",
"style": "width: 0%",
"percent": "0"
}
}
//处理特殊时间格式
const percent = this.getRemainingPercent(startDate, endDate, note);
return this.formatPercents(online, this.toFixed2(percent));
},
getRemainingDays(endDate, note) {
// 检查 endDate 是否有效
if (!endDate || typeof endDate !== 'string') {
return null;
}
// 处理特殊时间格式
if (endDate.includes("0000-00-00")) {
return "lifetime";
}
// 检查 startDate 和 endDate 是否为合法的Date
if (isNaN(new Date(endDate).getTime())) {
return "NaN";
}
// 获取当前时间,并调整时区
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 (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
return null;
}
// 处理特殊时间格式
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) {
return 100;
}
// 检查 startDate 和 endDate 是否为合法的Date
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
return "NaN";
}
// 获取当前时间并调整时区
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.01; //避免当条件判断!percent被过滤
}
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);
} }
} }
}) })
+4 -15
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>
@@ -64,6 +53,6 @@
</nav> </nav>
</div> </div>
</header> </header>
<div class="nezha"> <section class="nezha">
{{end}} {{end}}
+49 -46
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,37 +67,47 @@
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 = 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;
const legendLeft = this.isMobile ? 'center' : 'center'; const legendLeft = this.isMobile ? 'center' : 'center';
const legendTop = this.isMobile ? 5 : 5; const legendTop = this.isMobile ? 5 : 5;
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0]; const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000"; const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
const chartTheme = this.theme == "dark" ? "dark" : "default";
const backgroundColor = this.theme == "dark" ? '' : ''; const backgroundColor = this.theme == "dark" ? '' : '';
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)"); const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)"); const 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 splitLineColor = this.theme == "dark" ? "rgba(110, 112, 121, 0.95)" : "rgba(224, 230, 241, 0.95)"; const markPointSymbolSize = this.isMobile ? 36 : 42;
const markPointLabelColor = this.theme == "dark" ? "#111111" : "#000000"; const markPointItemStyleOpacity = this.semiTransparent ? 1 : 1;
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 0.75; 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;
this.chart = echarts.init(chartContainer, '', { // init图表 const showLoadingMaskColor = this.theme == "dark" ? 'rgba(0, 0, 0, 0)' : 'rgba(255, 255, 255, 0)';
const showLoadingTextColor = this.theme == "dark" ? 'rgba(241, 241, 241, 1)' : 'rgba(0, 0, 0, 1)';
const showLoadingColor = this.theme == "dark" ? '#D2B206' : '#FFDF32';
this.chart = echarts.init(chartContainer, chartTheme, { // init图表
renderer: 'canvas', 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)=> {
@@ -111,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 },
@@ -142,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,
@@ -182,10 +188,9 @@
itemStyle: { itemStyle: {
opacity: markPointItemStyleOpacity opacity: markPointItemStyleOpacity
}, },
symbolSize: 30, symbolSize: markPointSymbolSize,
label: { label: {
fontSize: 8, fontSize: markPointFontSize,
color: markPointLabelColor,
formatter: function (params) { formatter: function (params) {
return Math.round(params.value); return Math.round(params.value);
} }
@@ -198,10 +203,9 @@
itemStyle: { itemStyle: {
opacity: markPointItemStyleOpacity opacity: markPointItemStyleOpacity
}, },
symbolSize: 30, symbolSize: markPointSymbolSize,
label: { label: {
fontSize: 8, fontSize: markPointFontSize,
color: markPointLabelColor,
offset: [0, 8], offset: [0, 8],
formatter: function (params) { formatter: function (params) {
return Math.round(params.value); return Math.round(params.value);
@@ -218,7 +222,8 @@
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 = autoheight + autoIncrement; const height = autoheight + autoIncrement;
const gridTop = 40 + autoIncrement; const gridTop = this.isMobile ? ( 60 + autoIncrement) : (80 + autoIncrement);
const gridBottom = this.isMobile ? 70 : 90;
const legendIcon = this.isMobile ? 'rect' : ""; const legendIcon = this.isMobile ? 'rect' : "";
const itemWidth = this.isMobile ? 10 : 25; const itemWidth = this.isMobile ? 10 : 25;
const itemHeight = this.isMobile ? 10 : 14; const itemHeight = this.isMobile ? 10 : 14;
@@ -228,28 +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
}, },
// 图表标题设置
title: { title: {
show: false, show: false,
}, },
// 图表系列数据设置
series: seriesData.flat(), series: seriesData.flat(),
// X轴设置
xAxis: { xAxis: {
type: 'time', type: 'time',
axisLabel: { axisLabel: {
@@ -258,7 +257,6 @@
} }
} }
}, },
// Y轴设置
yAxis: { yAxis: {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
@@ -268,12 +266,10 @@
}, },
splitLine: { splitLine: {
lineStyle: { lineStyle: {
width: splitLineWidth, width: splitLineWidth
color: splitLineColor
} }
} }
}, },
// 图例设置
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();
+10 -10
View File
@@ -3,24 +3,24 @@
{{template "theme-server-status/menu" .}} {{template "theme-server-status/menu" .}}
<!-- showGroup true --> <!-- showGroup true -->
<template v-if="showGroup"> <template v-if="showGroup">
<section v-if="servicesTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;"> <div v-if="servicesTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p> <p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
</section> </div>
<section v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;" v-for="group in servicesTag"> <div v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
{{template "theme-server-status/service-group-true" .}} {{template "theme-server-status/service-group-true" .}}
</section> </div>
</template> </template>
<!-- showGroup false --> <!-- showGroup false -->
<template v-else> <template v-else>
<section v-if="servicesNoTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;"> <div v-if="servicesNoTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p> <p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
</section> </div>
<section v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;"> <div v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
{{template "theme-server-status/service-group-false" .}} {{template "theme-server-status/service-group-false" .}}
</section> </div>
</template> </template>
{{if .CycleTransferStats}} {{if .CycleTransferStats}}
<section class="container-fluid content table-responsive"> <div class="container-fluid content table-responsive">
<table class="table table-striped table-condensed table-hover"> <table class="table table-striped table-condensed table-hover">
<thead> <thead>
<tr class="node-group-tag"> <tr class="node-group-tag">
@@ -67,7 +67,7 @@
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</section> </div>
{{end}} {{end}}
{{template "theme-server-status/footer" .}} {{template "theme-server-status/footer" .}}
<script> <script>
+14 -10
View File
@@ -12,7 +12,7 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service" NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard" NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.18.3" NZ_VERSION="v0.19.2"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
@@ -65,6 +65,8 @@ geo_check() {
} }
pre_check() { pre_check() {
umask 077
## os_arch ## os_arch
if uname -m | grep -q 'x86_64'; then if uname -m | grep -q 'x86_64'; then
os_arch="amd64" os_arch="amd64"
@@ -288,8 +290,6 @@ install_dashboard() {
esac esac
fi fi
sudo chmod -R 700 $NZ_DASHBOARD_PATH
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
install_dashboard_docker install_dashboard_docker
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
@@ -375,7 +375,6 @@ install_agent() {
# 哪吒监控文件夹 # 哪吒监控文件夹
sudo mkdir -p $NZ_AGENT_PATH sudo mkdir -p $NZ_AGENT_PATH
sudo chmod -R 700 $NZ_AGENT_PATH
echo "正在下载监控端" echo "正在下载监控端"
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
@@ -454,11 +453,16 @@ modify_dashboard_config() {
echo "> 修改面板配置" echo "> 修改面板配置"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
echo "正在下载 Docker 脚本" if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1 echo "正在下载 Docker 脚本"
if [ $? != 0 ]; then wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}" if [ $? != 0 ]; then
return 0 err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
fi
else
err "请手动安装 docker-compose。https://docs.docker.com/compose/install/linux/"
before_show_menu
fi fi
fi fi
@@ -604,7 +608,7 @@ restart_and_update_standalone() {
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip" NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
fi fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard sudo systemctl enable nezha-dashboard
+14 -10
View File
@@ -12,7 +12,7 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service" NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard" NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.18.3" NZ_VERSION="v0.19.2"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
@@ -65,6 +65,8 @@ geo_check() {
} }
pre_check() { pre_check() {
umask 077
## os_arch ## os_arch
if uname -m | grep -q 'x86_64'; then if uname -m | grep -q 'x86_64'; then
os_arch="amd64" os_arch="amd64"
@@ -287,8 +289,6 @@ install_dashboard() {
esac esac
fi fi
sudo chmod -R 700 $NZ_DASHBOARD_PATH
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
install_dashboard_docker install_dashboard_docker
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
@@ -374,7 +374,6 @@ install_agent() {
# Nezha Monitoring Folder # Nezha Monitoring Folder
sudo mkdir -p $NZ_AGENT_PATH sudo mkdir -p $NZ_AGENT_PATH
sudo chmod -R 700 $NZ_AGENT_PATH
echo "Downloading Agent" echo "Downloading Agent"
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1 wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1
@@ -448,11 +447,16 @@ modify_dashboard_config() {
echo "> Modify Dashboard Configuration" echo "> Modify Dashboard Configuration"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
echo "Download Docker Script" if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1 echo "Download Docker Script"
if [ $? != 0 ]; then wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}" if [ $? != 0 ]; then
return 0 err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
fi
else
err "请手动安装 docker-compose。https://docs.docker.com/compose/install/linux/"
before_show_menu
fi fi
fi fi
@@ -598,7 +602,7 @@ restart_and_update_standalone() {
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip" NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
fi fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard sudo systemctl enable nezha-dashboard
+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
} }
+1 -1
View File
@@ -155,8 +155,8 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
Ipv4Addr: ipv4, Ipv4Addr: ipv4,
Ipv6Addr: ipv6, Ipv6Addr: ipv6,
} }
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
} else { } else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG // 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile) log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)