diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index 095a06a20..330f1d00d 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -1,10 +1,13 @@ package v1 import ( + "errors" + "github.com/1Panel-dev/1Panel/app/api/v1/helper" "github.com/1Panel-dev/1Panel/app/dto" "github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/global" + "github.com/1Panel-dev/1Panel/utils/mfa" "github.com/1Panel-dev/1Panel/utils/ntp" "github.com/gin-gonic/gin" ) @@ -70,3 +73,55 @@ func (b *BaseApi) SyncTime(c *gin.Context) { helper.SuccessWithData(c, ts) } + +func (b *BaseApi) CleanMonitor(c *gin.Context) { + if err := global.DB.Exec("DELETE FROM monitor_bases").Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if err := global.DB.Exec("DELETE FROM monitor_ios").Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if err := global.DB.Exec("DELETE FROM monitor_networks").Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +func (b *BaseApi) GetMFA(c *gin.Context) { + otp, err := mfa.GetOtp("admin") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, otp) +} + +func (b *BaseApi) MFABind(c *gin.Context) { + var req dto.MfaCredential + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + success := mfa.ValidCode(req.Code, req.Secret) + if !success { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, errors.New("code is not valid")) + return + } + + if err := settingService.Update(c, "MFAStatus", "enable"); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + if err := settingService.Update(c, "MFASecret", req.Secret); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index 4f1cf2a01..b23dd39d1 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -16,6 +16,7 @@ type SettingInfo struct { PasswordTimeOut string `json:"passwordTimeOut"` ComplexityVerification string `json:"complexityVerification"` MFAStatus string `json:"mfaStatus"` + MFASecret string `json:"mfaSecret"` MonitorStatus string `json:"monitorStatus"` MonitorStoreDays string `json:"monitorStoreDays"` diff --git a/backend/app/dto/user.go b/backend/app/dto/user.go index 4f631a4cb..e08f5a69b 100644 --- a/backend/app/dto/user.go +++ b/backend/app/dto/user.go @@ -9,3 +9,8 @@ type UserLoginInfo struct { Name string `json:"name"` Token string `json:"token"` } + +type MfaCredential struct { + Secret string `json:"secret"` + Code string `json:"code"` +} diff --git a/backend/app/model/setting.go b/backend/app/model/setting.go index f49d0692c..e9791b79b 100644 --- a/backend/app/model/setting.go +++ b/backend/app/model/setting.go @@ -1,9 +1,7 @@ package model -import "gorm.io/gorm" - type Setting struct { - gorm.Model + BaseModel Key string `json:"key" gorm:"type:varchar(256);not null;"` Value string `json:"value" gorm:"type:varchar(256)"` About string `json:"about" gorm:"type:longText"` diff --git a/backend/cron/job/monitor.go b/backend/cron/job/monitor.go index 008242e80..712aa5088 100644 --- a/backend/cron/job/monitor.go +++ b/backend/cron/job/monitor.go @@ -1,9 +1,11 @@ package job import ( + "strconv" "time" "github.com/1Panel-dev/1Panel/app/model" + "github.com/1Panel-dev/1Panel/app/repo" "github.com/1Panel-dev/1Panel/global" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/disk" @@ -19,6 +21,11 @@ func NewMonitorJob() *monitor { } func (m *monitor) Run() { + settingRepo := repo.NewISettingRepo() + monitorStatus, _ := settingRepo.Get(settingRepo.WithByKey("MonitorStatus")) + if monitorStatus.Value == "disable" { + return + } var itemModel model.MonitorBase totalPercent, _ := cpu.Percent(3*time.Second, false) if len(totalPercent) == 1 { @@ -35,7 +42,9 @@ func (m *monitor) Run() { memoryInfo, _ := mem.VirtualMemory() itemModel.Memory = memoryInfo.UsedPercent - _ = global.DB.Create(&itemModel) + if err := global.DB.Create(&itemModel).Error; err != nil { + global.LOG.Debug("create monitor base failed, err: %v", err) + } ioStat, _ := disk.IOCounters() for _, v := range ioStat { @@ -67,7 +76,9 @@ func (m *monitor) Run() { if writeTime > itemIO.Time { itemIO.Time = writeTime } - _ = global.DB.Create(&itemIO) + if err := global.DB.Create(&itemIO).Error; err != nil { + global.LOG.Debug("create monitor io failed, err: %v", err) + } } netStat, _ := net.IOCounters(true) @@ -84,7 +95,9 @@ func (m *monitor) Run() { stime := time.Since(aheadData.CreatedAt).Seconds() itemNet.Up = float64(v.BytesSent-aheadData.BytesSent) / 1024 / stime itemNet.Down = float64(v.BytesRecv-aheadData.BytesRecv) / 1024 / stime - _ = global.DB.Create(&itemNet) + if err := global.DB.Create(&itemNet).Error; err != nil { + global.LOG.Debug("create monitor network failed, err: %v", err) + } } netStatAll, _ := net.IOCounters(false) if len(netStatAll) != 0 { @@ -100,6 +113,18 @@ func (m *monitor) Run() { stime := time.Since(aheadData.CreatedAt).Seconds() itemNet.Up = float64(netStatAll[0].BytesSent-aheadData.BytesSent) / 1024 / stime itemNet.Down = float64(netStatAll[0].BytesRecv-aheadData.BytesRecv) / 1024 / stime - _ = global.DB.Create(&itemNet) + if err := global.DB.Create(&itemNet).Error; err != nil { + global.LOG.Debug("create monitor network all failed, err: %v", err) + } } + + MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays")) + if err != nil { + return + } + storeDays, _ := strconv.Atoi(MonitorStoreDays.Value) + timeForDelete := time.Now().AddDate(0, 0, -storeDays) + _ = global.DB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorBase{}).Error + _ = global.DB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorIO{}).Error + _ = global.DB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error } diff --git a/backend/go.mod b/backend/go.mod index 6fdfe5d6a..daa2bebce 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -27,11 +27,13 @@ require ( github.com/satori/go.uuid v1.2.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/sirupsen/logrus v1.9.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/afero v1.8.2 github.com/spf13/viper v1.12.0 github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/gin-swagger v1.5.1 github.com/swaggo/swag v1.8.4 + github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 golang.org/x/text v0.3.7 gopkg.in/yaml.v2 v2.4.0 diff --git a/backend/go.sum b/backend/go.sum index 5e9005277..ac08fd708 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -371,6 +371,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -430,6 +432,8 @@ github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f h1:C8De+7emQKojPBC+mXA0fr39XN5mKjRm9IUzdxI4whI= +github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index af51d4564..6fe8e871f 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -93,6 +93,9 @@ var AddTableSetting = &gormigrate.Migration{ if err := tx.Create(&model.Setting{Key: "MFAStatus", Value: "disable"}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "MFASecret", Value: ""}).Error; err != nil { + return err + } if err := tx.Create(&model.Setting{Key: "MonitorStatus", Value: "enable"}).Error; err != nil { return err diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index 820109eda..3842b1ad7 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -17,5 +17,8 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { withRecordRouter.PUT("", baseApi.UpdateSetting) settingRouter.PUT("/password", baseApi.UpdatePassword) settingRouter.POST("/time/sync", baseApi.SyncTime) + settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) + settingRouter.GET("/mfa", baseApi.GetMFA) + settingRouter.POST("/mfa/bind", baseApi.MFABind) } } diff --git a/backend/utils/mfa/mfa.go b/backend/utils/mfa/mfa.go new file mode 100644 index 000000000..76ac8a957 --- /dev/null +++ b/backend/utils/mfa/mfa.go @@ -0,0 +1,40 @@ +package mfa + +import ( + "bytes" + "encoding/base64" + "strconv" + "time" + + "github.com/skip2/go-qrcode" + "github.com/xlzd/gotp" +) + +const secretLength = 16 + +type Otp struct { + Secret string `json:"secret"` + QrImage string `json:"qrImage"` +} + +func GetOtp(username string) (otp Otp, err error) { + secret := gotp.RandomSecret(secretLength) + otp.Secret = secret + totp := gotp.NewDefaultTOTP(secret) + uri := totp.ProvisioningUri(username, "1Panel") + subImg, err := qrcode.Encode(uri, qrcode.Medium, 256) + dist := make([]byte, 3000) + base64.StdEncoding.Encode(dist, subImg) + index := bytes.IndexByte(dist, 0) + baseImage := dist[0:index] + otp.QrImage = "data:image/png;base64," + string(baseImage) + return +} + +func ValidCode(code string, secret string) bool { + totp := gotp.NewDefaultTOTP(secret) + now := time.Now().Unix() + strInt64 := strconv.FormatInt(now, 10) + id16, _ := strconv.Atoi(strInt64) + return totp.Verify(code, int64(id16)) +} diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index a587b6c30..cdd6de13d 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -16,6 +16,7 @@ export namespace Setting { passwordTimeOut: string; complexityVerification: string; mfaStatus: string; + mfaSecret: string; monitorStatus: string; monitorStoreDays: string; @@ -34,4 +35,12 @@ export namespace Setting { newPassword: string; retryPassword: string; } + export interface MFAInfo { + secret: string; + qrImage: string; + } + export interface MFABind { + secret: string; + code: string; + } } diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index c469a940d..a26dad40a 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -16,3 +16,15 @@ export const updatePassword = (param: Setting.PasswordUpdate) => { export const syncTime = () => { return http.post(`/settings/time/sync`, {}); }; + +export const cleanMonitors = () => { + return http.post(`/settings/monitor/clean`, {}); +}; + +export const getMFA = () => { + return http.get(`/settings/mfa`, {}); +}; + +export const bindMFA = (param: Setting.MFABind) => { + return http.post(`/settings/mfa/bind`, param); +}; diff --git a/frontend/src/global/form-rues.ts b/frontend/src/global/form-rues.ts index 8704486a6..5b70c4ec0 100644 --- a/frontend/src/global/form-rues.ts +++ b/frontend/src/global/form-rues.ts @@ -20,6 +20,7 @@ interface CommonRule { requiredSelect: FormItemRule; name: FormItemRule; email: FormItemRule; + number: FormItemRule; ip: FormItemRule; port: FormItemRule; } @@ -48,6 +49,13 @@ export const Rules: CommonRule = { message: i18n.global.t('commons.rule.email'), trigger: 'blur', }, + number: { + required: true, + trigger: 'blur', + min: 0, + type: 'number', + message: i18n.global.t('commons.rule.number'), + }, ip: { validator: checkIp, required: true, diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index c870a2c33..b9abbe69c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -11,8 +11,11 @@ export default { confirm: 'Confirm', cancel: 'Cancel', reset: 'Reset', - login: 'Login', conn: 'Connect', + clean: 'Clean', + login: 'Login', + close: 'Close', + saveAndEnable: 'Save and enable', }, search: { timeStart: 'Time start', @@ -268,5 +271,35 @@ export default { oldPassword: 'Original password', newPassword: 'New password', retryPassword: 'Confirm password', + safe: 'Safe', + panelPort: 'Panel port', + portHelper: + 'The recommended port range is 8888 to 65535. Note: If the server has a security group, permit the new port from the security group in advance', + safeEntrance: 'Security entrance', + safeEntranceHelper: + 'Panel management portal. You can log in to the panel only through a specified security portal, for example, / 89DC6AE8', + passwordTimeout: 'Password expiration Time', + timeoutHelper: + '[ {0} days ] The panel password is about to expire. After the expiration, you need to reset the password', + complexity: 'Password complexity verification', + complexityHelper: + 'The password must contain at least eight characters and contain at least three uppercase letters, lowercase letters, digits, and special characters', + mfa: 'MFA', + mfaHelper1: 'Download a MFA verification mobile app such as:', + mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code', + mfaHelper3: 'Enter six digits from the app', + enableMonitor: 'Enable', + storeDays: 'Expiration time (day)', + cleanMonitor: 'Clearing monitoring records', + message: 'Message', + messageType: 'Message type', + email: 'Email', + wechat: 'WeChat', + dingding: 'DingDing', + closeMessage: 'Turning off Message Notification', + emailServer: 'Service name', + emailAddr: 'Service address', + emailSMTP: 'SMTP code', + secret: 'Secret', }, }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 554c8ac2b..f1db3ddd5 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -8,11 +8,16 @@ export default { sync: '同步', delete: '删除', edit: '编辑', + enable: '启用', + disable: '禁用', confirm: '确认', cancel: '取消', reset: '重置', conn: '连接', + clean: '清空', login: '登录', + close: '关闭', + saveAndEnable: '保存并启用', }, search: { timeStart: '开始时间', @@ -64,7 +69,8 @@ export default { requiredInput: '请填写必填项', requiredSelect: '请选择必选项', commonName: '支持英文、中文、数字、.-_,长度1-30', - email: '邮箱格式错误', + email: '请输入正确的邮箱', + number: '请输入正确的数字', ip: '请输入正确的 IP 地址', port: '请输入正确的端口', }, @@ -277,5 +283,31 @@ export default { oldPassword: '原密码', newPassword: '新密码', retryPassword: '确认密码', + safe: '安全', + panelPort: '面板端口', + portHelper: '建议端口范围8888 - 65535,注意:有安全组的服务器请提前在安全组放行新端口', + safeEntrance: '安全入口', + safeEntranceHelper: '面板管理入口,设置后只能通过指定安全入口登录面板,如: /89dc6ae8', + passwordTimeout: '密码过期时间', + timeoutHelper: '【 {0} 天后 】面板密码即将过期,过期后需要重新设置密码', + complexity: '密码复杂度验证', + complexityHelper: '密码必须满足密码长度大于8位且大写字母、小写字母、数字、特殊字符至少3项组合', + mfa: '两步验证', + mfaHelper1: '下载两步验证手机应用 如:', + mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码', + mfaHelper3: '输入手机应用上的 6 位数字', + enableMonitor: '监控状态', + storeDays: '过期时间 (天)', + cleanMonitor: '清空监控记录', + message: '通知', + messageType: '通知方式', + email: '邮箱', + wechat: '企业微信', + dingding: '钉钉', + closeMessage: '关闭消息通知', + emailServer: '邮箱服务名称', + emailAddr: '邮箱地址', + emailSMTP: '邮箱 SMTP 授权码', + secret: '密钥', }, }; diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index 18d6ebe07..3673dcf83 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -29,7 +29,7 @@ const hostRouter = { { path: '/hosts/monitor', name: 'Monitor', - component: () => import('@/views/monitor/index.vue'), + component: () => import('@/views/host/monitor/index.vue'), meta: { title: 'menu.monitor', }, diff --git a/frontend/src/views/host/monitor/index.vue b/frontend/src/views/host/monitor/index.vue index 0c32f66cc..cbbab7e0b 100644 --- a/frontend/src/views/host/monitor/index.vue +++ b/frontend/src/views/host/monitor/index.vue @@ -9,7 +9,6 @@ @change="search('load')" v-model="timeRangeLoad" type="datetimerange" - size="small" :range-separator="$t('commons.search.timeRange')" :start-placeholder="$t('commons.search.timeStart')" :end-placeholder="$t('commons.search.timeEnd')" @@ -30,7 +29,6 @@ @change="search('cpu')" v-model="timeRangeCpu" type="datetimerange" - size="small" :range-separator="$t('commons.search.timeRange')" :start-placeholder="$t('commons.search.timeStart')" :end-placeholder="$t('commons.search.timeEnd')" @@ -49,7 +47,6 @@ @change="search('memory')" v-model="timeRangeMemory" type="datetimerange" - size="small" :range-separator="$t('commons.search.timeRange')" :start-placeholder="$t('commons.search.timeStart')" :end-placeholder="$t('commons.search.timeEnd')" @@ -70,7 +67,6 @@ @change="search('io')" v-model="timeRangeIO" type="datetimerange" - size="small" :range-separator="$t('commons.search.timeRange')" :start-placeholder="$t('commons.search.timeStart')" :end-placeholder="$t('commons.search.timeEnd')" @@ -92,7 +88,6 @@ @change="search('network')" style="margin-left: 20px" placeholder="Select" - size="small" > @@ -100,7 +95,6 @@ @change="search('network')" v-model="timeRangeNetwork" type="datetimerange" - size="small" :range-separator="$t('commons.search.timeRange')" :start-placeholder="$t('commons.search.timeStart')" :end-placeholder="$t('commons.search.timeEnd')" diff --git a/frontend/src/views/host/terminal/host/index.vue b/frontend/src/views/host/terminal/host/index.vue index c09abd323..e5a82af94 100644 --- a/frontend/src/views/host/terminal/host/index.vue +++ b/frontend/src/views/host/terminal/host/index.vue @@ -3,33 +3,21 @@ - + - + - + - + - + - + diff --git a/frontend/src/views/host/terminal/index.vue b/frontend/src/views/host/terminal/index.vue index 0551e6d53..e136d8405 100644 --- a/frontend/src/views/host/terminal/index.vue +++ b/frontend/src/views/host/terminal/index.vue @@ -47,7 +47,7 @@ @change="quickInput" style="width: 25%" :placeholder="$t('terminal.quickCommand')" - size="small" + > @@ -81,7 +81,7 @@ New ssh New tab - + - +
- - - - 关闭 - email - 企业微信 - 钉钉 + + + + {{ $t('commons.button.close') }} + {{ $t('setting.email') }} + {{ $t('setting.wechat') }} + {{ $t('setting.dingding') }} -
+
- 关闭消息通知 + {{ $t('setting.closeMessage') }}
-
- - +
+ + - - + + - - + + - 保存并启用 + {{ $t('commons.button.saveAndEnable') }}
-
+
- + - + - + - + - 保存并启用 + {{ $t('commons.button.saveAndEnable') }}
-
+
- + - - - - - + + - 保存并启用 + {{ $t('commons.button.saveAndEnable') }}
@@ -74,7 +71,7 @@ diff --git a/frontend/src/views/setting/tabs/monitor.vue b/frontend/src/views/setting/tabs/monitor.vue index 595161631..5c6e5f066 100644 --- a/frontend/src/views/setting/tabs/monitor.vue +++ b/frontend/src/views/setting/tabs/monitor.vue @@ -1,36 +1,37 @@