mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +08:00
feat: Merge code from dev
This commit is contained in:
parent
bb250557a2
commit
2920aa3588
20
agent/app/dto/alert.go
Normal file
20
agent/app/dto/alert.go
Normal file
@ -0,0 +1,20 @@
|
||||
package dto
|
||||
|
||||
type CreateOrUpdateAlert struct {
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertType string `json:"alertType"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
EntryID uint `json:"entryID"`
|
||||
}
|
||||
|
||||
type AlertBase struct {
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
}
|
||||
|
||||
type PushAlert struct {
|
||||
TaskName string `json:"taskName"`
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
Param string `json:"param"`
|
||||
}
|
@ -33,6 +33,7 @@ type ClamInfo struct {
|
||||
LastHandleDate string `json:"lastHandleDate"`
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
}
|
||||
|
||||
type ClamLogSearch struct {
|
||||
@ -71,6 +72,8 @@ type ClamCreate struct {
|
||||
InfectedDir string `json:"infectedDir"`
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type ClamUpdate struct {
|
||||
@ -82,6 +85,8 @@ type ClamUpdate struct {
|
||||
InfectedDir string `json:"infectedDir"`
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type ClamUpdateStatus struct {
|
||||
|
@ -41,10 +41,14 @@ type CronjobCreate struct {
|
||||
DownloadAccountID uint `json:"downloadAccountID"`
|
||||
RetainCopies int `json:"retainCopies" validate:"number,min=1"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type CronjobUpdate struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
SpecCustom bool `json:"specCustom"`
|
||||
Spec string `json:"spec" validate:"required"`
|
||||
@ -69,6 +73,9 @@ type CronjobUpdate struct {
|
||||
DownloadAccountID uint `json:"downloadAccountID"`
|
||||
RetainCopies int `json:"retainCopies" validate:"number,min=1"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type CronjobUpdateStatus struct {
|
||||
@ -122,6 +129,8 @@ type CronjobInfo struct {
|
||||
LastRecordTime string `json:"lastRecordTime"`
|
||||
Status string `json:"status"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
}
|
||||
|
||||
type SearchRecord struct {
|
||||
|
@ -3,15 +3,17 @@ package service
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
@ -155,6 +157,16 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa
|
||||
}
|
||||
datas[i].LastHandleDate = t1.Format(constant.DateTimeLayout)
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: "clams",
|
||||
EntryID: datas[i].ID,
|
||||
}
|
||||
alertCount := xpack.GetAlert(alertBase)
|
||||
if alertCount != 0 {
|
||||
datas[i].AlertCount = alertCount
|
||||
} else {
|
||||
datas[i].AlertCount = 0
|
||||
}
|
||||
}
|
||||
return total, datas, err
|
||||
}
|
||||
@ -181,6 +193,18 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
|
||||
if err := clamRepo.Create(&clam); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
createAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertCount: req.AlertCount,
|
||||
AlertType: "clams",
|
||||
EntryID: clam.ID,
|
||||
}
|
||||
err := xpack.CreateAlert(createAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -226,6 +250,16 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
|
||||
if err := clamRepo.Update(req.ID, upMap); err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertType: "clams",
|
||||
AlertCount: req.AlertCount,
|
||||
EntryID: clam.ID,
|
||||
}
|
||||
err := xpack.UpdateAlert(updateAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -266,6 +300,14 @@ func (c *ClamService) Delete(req dto.ClamDelete) error {
|
||||
if err := clamRepo.Delete(repo.WithByID(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: "clams",
|
||||
EntryID: clam.ID,
|
||||
}
|
||||
err := xpack.DeleteAlert(alertBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -309,6 +351,7 @@ func (c *ClamService) HandleOnce(req dto.OperateByID) error {
|
||||
if err != nil {
|
||||
global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err)
|
||||
}
|
||||
handleAlert(stdout, clam.Name, clam.ID)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@ -586,3 +629,28 @@ func (c *ClamService) loadLogPath(name string) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleAlert(stdout, clamName string, clamId uint) {
|
||||
if strings.Contains(stdout, "- SCAN SUMMARY -") {
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Infected files: ") {
|
||||
var infectedFiles = 0
|
||||
infectedFiles, _ = strconv.Atoi(strings.TrimPrefix(line, "Infected files: "))
|
||||
if infectedFiles > 0 {
|
||||
pushAlert := dto.PushAlert{
|
||||
TaskName: clamName,
|
||||
AlertType: "clams",
|
||||
EntryID: clamId,
|
||||
Param: strconv.Itoa(infectedFiles),
|
||||
}
|
||||
err := xpack.PushAlert(pushAlert)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("clamdscan push failed, err: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -812,8 +812,7 @@ func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
||||
}
|
||||
return true
|
||||
case err := <-errorChan:
|
||||
errorMsg := fmt.Sprintf("event: error\ndata: %v\n\n", err.Error())
|
||||
_, err = fmt.Fprintf(w, errorMsg)
|
||||
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error())
|
||||
return false
|
||||
case <-ctx.Request.Context().Done():
|
||||
return false
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@ -58,6 +59,16 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
||||
item.LastRecordTime = "-"
|
||||
}
|
||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertCount := xpack.GetAlert(alertBase)
|
||||
if alertCount != 0 {
|
||||
item.AlertCount = alertCount
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
}
|
||||
dtoCronjobs = append(dtoCronjobs, item)
|
||||
}
|
||||
return total, dtoCronjobs, err
|
||||
@ -208,6 +219,18 @@ func (u *CronjobService) Create(req dto.CronjobCreate) error {
|
||||
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
createAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertCount: req.AlertCount,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
err := xpack.CreateAlert(createAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -250,6 +273,14 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
||||
if err := cronjobRepo.Delete(repo.WithByID(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
err := xpack.DeleteAlert(alertBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -303,7 +334,21 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
|
||||
upMap["download_account_id"] = req.DownloadAccountID
|
||||
upMap["retain_copies"] = req.RetainCopies
|
||||
upMap["secret"] = req.Secret
|
||||
return cronjobRepo.Update(id, upMap)
|
||||
err = cronjobRepo.Update(id, upMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertType: cronModel.Type,
|
||||
AlertCount: req.AlertCount,
|
||||
EntryID: cronModel.ID,
|
||||
}
|
||||
err = xpack.UpdateAlert(updateAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
@ -315,6 +360,7 @@ func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
entryIDs string
|
||||
err error
|
||||
)
|
||||
|
||||
if status == constant.StatusEnable {
|
||||
entryIDs, err = u.StartJob(&cronjob, false)
|
||||
if err != nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
@ -18,6 +19,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
)
|
||||
|
||||
func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
@ -69,6 +71,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
record.Records, _ = mkdirAndWriteFile(cronjob, record.StartTime, message)
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
return
|
||||
}
|
||||
if len(message) != 0 {
|
||||
@ -315,3 +318,17 @@ func (u *CronjobService) removeExpiredLog(cronjob model.Cronjob) {
|
||||
func hasBackup(cronjobType string) bool {
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log"
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
pushAlert := dto.PushAlert{
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
Param: cronjob.Type,
|
||||
}
|
||||
err := xpack.PushAlert(pushAlert)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("cronjob alert push failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
network "net"
|
||||
"os"
|
||||
"sort"
|
||||
@ -11,6 +10,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
@ -122,7 +123,7 @@ func (u *DashboardService) LoadCurrentInfoForNode() *dto.NodeCurrent {
|
||||
var currentInfo dto.NodeCurrent
|
||||
|
||||
currentInfo.CPUTotal, _ = cpu.Counts(true)
|
||||
totalPercent, _ := cpu.Percent(0, false)
|
||||
totalPercent, _ := cpu.Percent(100*time.Millisecond, false)
|
||||
if len(totalPercent) == 1 {
|
||||
currentInfo.CPUUsedPercent = totalPercent[0]
|
||||
currentInfo.CPUUsed = currentInfo.CPUUsedPercent * 0.01 * float64(currentInfo.CPUTotal)
|
||||
|
@ -202,6 +202,23 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
|
||||
daemonMap["exec-opts"] = []string{"native.cgroupdriver=systemd"}
|
||||
}
|
||||
}
|
||||
case "http-proxy", "https-proxy":
|
||||
delete(daemonMap, "proxies")
|
||||
if len(req.Value) > 0 {
|
||||
proxies := map[string]interface{}{
|
||||
req.Key: req.Value,
|
||||
}
|
||||
daemonMap["proxies"] = proxies
|
||||
}
|
||||
case "socks5-proxy", "close-proxy":
|
||||
delete(daemonMap, "proxies")
|
||||
if len(req.Value) > 0 {
|
||||
proxies := map[string]interface{}{
|
||||
"http-proxy": req.Value,
|
||||
"https-proxy": req.Value,
|
||||
}
|
||||
daemonMap["proxies"] = proxies
|
||||
}
|
||||
}
|
||||
if len(daemonMap) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
|
@ -65,9 +65,13 @@ func NewIFileService() IFileService {
|
||||
|
||||
func (f *FileService) GetFileList(op request.FileOption) (response.FileInfo, error) {
|
||||
var fileInfo response.FileInfo
|
||||
if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) {
|
||||
data, err := os.Stat(op.Path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return fileInfo, nil
|
||||
}
|
||||
if !data.IsDir() {
|
||||
op.FileOption.Path = filepath.Dir(op.FileOption.Path)
|
||||
}
|
||||
info, err := files.NewFileInfo(op.FileOption)
|
||||
if err != nil {
|
||||
return fileInfo, err
|
||||
@ -210,6 +214,12 @@ func (f *FileService) Create(op request.FileCreate) error {
|
||||
}
|
||||
|
||||
func (f *FileService) Delete(op request.FileDelete) error {
|
||||
if op.IsDir {
|
||||
excludeDir := global.CONF.System.DataDir
|
||||
if filepath.Base(op.Path) == ".1panel_clash" || op.Path == excludeDir {
|
||||
return buserr.New(constant.ErrPathNotDelete)
|
||||
}
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
recycleBinStatus, _ := settingRepo.Get(settingRepo.WithByKey("FileRecycleBin"))
|
||||
if recycleBinStatus.Value == "disable" {
|
||||
|
@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
@ -298,16 +299,20 @@ func StartMonitor(removeBefore bool, interval string) error {
|
||||
service := NewIMonitorService()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
monitorCancel = cancel
|
||||
now := time.Now()
|
||||
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
|
||||
time.AfterFunc(time.Until(nextMinute), func() {
|
||||
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %sm", interval), service)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
global.MonitorCronID = monitorID
|
||||
})
|
||||
|
||||
service.Run()
|
||||
|
||||
go service.saveIODataToDB(ctx, float64(intervalItem))
|
||||
go service.saveNetDataToDB(ctx, float64(intervalItem))
|
||||
|
||||
global.MonitorCronID = monitorID
|
||||
return nil
|
||||
}
|
||||
|
11
agent/constant/alert.go
Normal file
11
agent/constant/alert.go
Normal file
@ -0,0 +1,11 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
AlertEnable = "Enable"
|
||||
AlertDisable = "Disable"
|
||||
AlertSuccess = "Success"
|
||||
AlertError = "Error"
|
||||
AlertSyncError = "SyncError"
|
||||
AlertPushError = "PushError"
|
||||
AlertPushSuccess = "PushSuccess"
|
||||
)
|
@ -85,6 +85,7 @@ var (
|
||||
ErrFileDownloadDir = "ErrFileDownloadDir"
|
||||
ErrCmdNotFound = "ErrCmdNotFound"
|
||||
ErrFavoriteExist = "ErrFavoriteExist"
|
||||
ErrPathNotDelete = "ErrPathNotDelete"
|
||||
)
|
||||
|
||||
// mysql
|
||||
@ -142,3 +143,12 @@ var (
|
||||
var (
|
||||
ErrNotExistUser = "ErrNotExistUser"
|
||||
)
|
||||
|
||||
// alert
|
||||
var (
|
||||
ErrAlert = "ErrAlert"
|
||||
ErrAlertPush = "ErrAlertPush"
|
||||
ErrAlertSave = "ErrAlertSave"
|
||||
ErrAlertSync = "ErrAlertSync"
|
||||
ErrAlertRemote = "ErrAlertRemote"
|
||||
)
|
||||
|
@ -96,6 +96,7 @@ ErrCmdNotFound: "{{ .name}} command does not exist, please install this command
|
||||
ErrSourcePathNotFound: "Source directory does not exist"
|
||||
ErrFavoriteExist: "This path has been collected"
|
||||
ErrInvalidChar: "Illegal characters are prohibited"
|
||||
ErrPathNotDelete: "The selected directory cannot be deleted"
|
||||
|
||||
#website
|
||||
ErrDomainIsExist: "Domain is already exist"
|
||||
@ -363,3 +364,10 @@ websiteDir: "Website directory"
|
||||
RecoverFailedStartRollBack: "Recovery failed, starting rollback"
|
||||
AppBackupFileIncomplete: "Backup file is incomplete; missing app.json or app.tar.gz file"
|
||||
AppAttributesNotMatch: "Application type or name does not match"
|
||||
|
||||
#alert
|
||||
ErrAlert: "Alert information format error, please check and try again!"
|
||||
ErrAlertPush: "Alert push error, please check and try again!"
|
||||
ErrAlertSave: "Alert save error, please check and try again!"
|
||||
ErrAlertSync: "Alert sync error, please check and try again!"
|
||||
ErrAlertRemote: "Remote alert error, please check and try again!"
|
@ -4,6 +4,10 @@ ErrRecordExist: "記錄已存在"
|
||||
ErrRecordNotFound: "記錄未找到"
|
||||
ErrStructTransform: "類型轉換失敗: {{ .detail }}"
|
||||
ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}"
|
||||
ErrApiConfigStatusInvalid: "API 接口禁止訪問: {{ .detail }}"
|
||||
ErrApiConfigKeyInvalid: "API 接口密钥錯誤: {{ .detail }}"
|
||||
ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}"
|
||||
ErrApiConfigDisable: "此接口禁止使用 API 接口調用: {{ .detail }}"
|
||||
|
||||
#common
|
||||
ErrNameIsExist: "名稱已存在"
|
||||
@ -96,6 +100,7 @@ ErrFileDownloadDir: "不支持下載文件夾"
|
||||
ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令"
|
||||
ErrSourcePathNotFound: "源目錄不存在"
|
||||
ErrFavoriteExist: "已收藏此路徑"
|
||||
ErrPathNotDelete: "所選目錄不可删除"
|
||||
|
||||
#website
|
||||
ErrDomainIsExist: "域名已存在"
|
||||
@ -364,3 +369,10 @@ websiteDir: "網站目錄"
|
||||
RecoverFailedStartRollBack: "恢復失敗,開始回滾"
|
||||
AppBackupFileIncomplete: "備份文件不完整,缺少 app.json 或 app.tar.gz 文件"
|
||||
AppAttributesNotMatch: "應用類型或名稱不一致"
|
||||
|
||||
# alert
|
||||
ErrAlert: "告警資訊格式錯誤,請檢查後重試!"
|
||||
ErrAlertPush: "告警資訊推送錯誤,請檢查後重試!"
|
||||
ErrAlertSave: "告警資訊保存錯誤,請檢查後重試!"
|
||||
ErrAlertSync: "告警資訊同步錯誤,請檢查後重試!"
|
||||
ErrAlertRemote: "告警資訊遠端錯誤,請檢查後重試!"
|
@ -95,6 +95,7 @@ ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令"
|
||||
ErrSourcePathNotFound: "源目录不存在"
|
||||
ErrFavoriteExist: "已收藏此路径"
|
||||
ErrInvalidChar: "禁止使用非法字符"
|
||||
ErrPathNotDelete: "所选目录不可删除"
|
||||
|
||||
#website
|
||||
ErrDomainIsExist: "域名已存在"
|
||||
@ -389,3 +390,10 @@ websiteDir: "网站目录"
|
||||
RecoverFailedStartRollBack: "恢复失败,开始回滚"
|
||||
AppBackupFileIncomplete: "备份文件不完整 缺少 app.json 或者 app.tar.gz 文件"
|
||||
AppAttributesNotMatch: "应用类型或者名称不一致"
|
||||
|
||||
#alert
|
||||
ErrAlert: "告警信息格式错误,请检查后重试!"
|
||||
ErrAlertPush: "告警信息推送错误,请检查后重试!"
|
||||
ErrAlertSave: "告警信息保存错误,请检查后重试!"
|
||||
ErrAlertSync: "告警信息同步错误,请检查后重试!"
|
||||
ErrAlertRemote: "告警信息远端错误,请检查后重试!"
|
@ -3,12 +3,14 @@ package hook
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@ -58,11 +60,24 @@ func handleSnapStatus() {
|
||||
}
|
||||
|
||||
func handleCronjobStatus() {
|
||||
_ = global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting).
|
||||
var jobRecords []model.JobRecords
|
||||
_ = global.DB.Where("status = ?", constant.StatusWaiting).Find(&jobRecords).Error
|
||||
for _, record := range jobRecords {
|
||||
err := global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constant.StatusFailed,
|
||||
"message": "the task was interrupted due to the restart of the 1panel service",
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to update job ID: %v, Error:%v", record.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var cronjob *model.Cronjob
|
||||
_ = global.DB.Where("id = ?", record.CronjobID).First(&cronjob).Error
|
||||
handleCronJobAlert(cronjob)
|
||||
}
|
||||
}
|
||||
|
||||
func loadLocalDir() {
|
||||
@ -73,3 +88,17 @@ func loadLocalDir() {
|
||||
}
|
||||
global.CONF.System.Backup = account.BackupPath
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
pushAlert := dto.PushAlert{
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
Param: cronjob.Type,
|
||||
}
|
||||
err := xpack.PushAlert(pushAlert)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("cronjob alert push failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package common
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
mathRand "math/rand"
|
||||
"net"
|
||||
@ -15,6 +14,8 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
@ -325,6 +326,30 @@ func IsValidIP(ip string) bool {
|
||||
return net.ParseIP(ip) != nil
|
||||
}
|
||||
|
||||
const (
|
||||
b = uint64(1)
|
||||
kb = 1024 * b
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
)
|
||||
|
||||
func FormatBytes(bytes uint64) string {
|
||||
switch {
|
||||
case bytes < kb:
|
||||
return fmt.Sprintf("%dB", bytes)
|
||||
case bytes < mb:
|
||||
return fmt.Sprintf("%.2fKB", float64(bytes)/float64(kb))
|
||||
case bytes < gb:
|
||||
return fmt.Sprintf("%.2fMB", float64(bytes)/float64(mb))
|
||||
default:
|
||||
return fmt.Sprintf("%.2fGB", float64(bytes)/float64(gb))
|
||||
}
|
||||
}
|
||||
|
||||
func FormatPercent(percent float64) string {
|
||||
return fmt.Sprintf("%.2f%%", percent)
|
||||
}
|
||||
|
||||
func GetLang(context *gin.Context) string {
|
||||
lang := context.GetHeader("Accept-Language")
|
||||
if strings.Contains(lang, "zh") {
|
||||
|
@ -677,6 +677,9 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
|
||||
func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error {
|
||||
if cType == Tar || cType == Zip || cType == TarGz {
|
||||
shellArchiver, err := NewShellArchiver(cType)
|
||||
if !f.Stat(dst) {
|
||||
_ = f.CreateDir(dst, 0755)
|
||||
}
|
||||
if err == nil {
|
||||
if err = shellArchiver.Extract(srcFile, dst, secret); err == nil {
|
||||
return nil
|
||||
|
@ -73,6 +73,9 @@ func NewFileInfo(op FileOption) (*FileInfo, error) {
|
||||
|
||||
info, err := appFs.Stat(op.Path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, buserr.New(constant.ErrLinkPathNotFound)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -102,7 +105,26 @@ func NewFileInfo(op FileOption) (*FileInfo, error) {
|
||||
}
|
||||
|
||||
if file.IsSymlink {
|
||||
file.LinkPath = GetSymlink(op.Path)
|
||||
linkPath := GetSymlink(op.Path)
|
||||
if !filepath.IsAbs(linkPath) {
|
||||
dir := filepath.Dir(op.Path)
|
||||
var err error
|
||||
linkPath, err = filepath.Abs(filepath.Join(dir, linkPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
file.LinkPath = linkPath
|
||||
targetInfo, err := appFs.Stat(linkPath)
|
||||
if err != nil {
|
||||
file.IsDir = false
|
||||
file.Mode = "-"
|
||||
file.User = "-"
|
||||
file.Group = "-"
|
||||
} else {
|
||||
file.IsDir = targetInfo.IsDir()
|
||||
}
|
||||
file.Extension = filepath.Ext(file.LinkPath)
|
||||
}
|
||||
if op.Expand {
|
||||
if err := handleExpansion(file, op); err != nil {
|
||||
@ -309,7 +331,26 @@ func (f *FileInfo) processFiles(files []FileSearchInfo, option FileOption) ([]*F
|
||||
file.FavoriteID = favorite.ID
|
||||
}
|
||||
if isSymlink {
|
||||
file.LinkPath = GetSymlink(fPath)
|
||||
linkPath := GetSymlink(fPath)
|
||||
if !filepath.IsAbs(linkPath) {
|
||||
dir := filepath.Dir(fPath)
|
||||
var err error
|
||||
linkPath, err = filepath.Abs(filepath.Join(dir, linkPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
file.LinkPath = linkPath
|
||||
targetInfo, err := file.Fs.Stat(linkPath)
|
||||
if err != nil {
|
||||
file.IsDir = false
|
||||
file.Mode = "-"
|
||||
file.User = "-"
|
||||
file.Group = "-"
|
||||
} else {
|
||||
file.IsDir = targetInfo.IsDir()
|
||||
}
|
||||
file.Extension = filepath.Ext(file.LinkPath)
|
||||
}
|
||||
if df.Size() > 0 {
|
||||
file.MimeType = GetMimeType(fPath)
|
||||
|
@ -19,7 +19,7 @@ func NewTarArchiver(compressType CompressType) ShellArchiver {
|
||||
}
|
||||
|
||||
func (t TarArchiver) Extract(FilePath string, dstDir string, secret string) error {
|
||||
return cmd.ExecCmd(fmt.Sprintf("%s %s %s -C %s", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir))
|
||||
return cmd.ExecCmd(fmt.Sprintf("%s %s \"%s\" -C \"%s\"", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir))
|
||||
}
|
||||
|
||||
func (t TarArchiver) Compress(sourcePaths []string, dstFile string, secret string) error {
|
||||
|
@ -20,11 +20,11 @@ func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error {
|
||||
var err error
|
||||
commands := ""
|
||||
if len(secret) != 0 {
|
||||
extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + filePath + " | "
|
||||
commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, dstDir+" > /dev/null 2>&1")
|
||||
extraCmd := fmt.Sprintf("openssl enc -d -aes-256-cbc -k '%s' -in '%s' | ", secret, filePath)
|
||||
commands = fmt.Sprintf("%s tar -zxvf - -C '%s' > /dev/null 2>&1", extraCmd, dstDir)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar -zxvf %s %s", filePath+" -C ", dstDir+" > /dev/null 2>&1")
|
||||
commands = fmt.Sprintf("tar -zxvf '%s' -C '%s' > /dev/null 2>&1", filePath, dstDir)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
if err = cmd.ExecCmd(commands); err != nil {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
@ -146,26 +147,6 @@ func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
b = uint64(1)
|
||||
kb = 1024 * b
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
)
|
||||
|
||||
func formatBytes(bytes uint64) string {
|
||||
switch {
|
||||
case bytes < kb:
|
||||
return fmt.Sprintf("%dB", bytes)
|
||||
case bytes < mb:
|
||||
return fmt.Sprintf("%.2fKB", float64(bytes)/float64(kb))
|
||||
case bytes < gb:
|
||||
return fmt.Sprintf("%.2fMB", float64(bytes)/float64(mb))
|
||||
default:
|
||||
return fmt.Sprintf("%.2fGB", float64(bytes)/float64(gb))
|
||||
}
|
||||
}
|
||||
|
||||
func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
|
||||
var processes []*process.Process
|
||||
processes, err = process.Processes()
|
||||
@ -229,14 +210,14 @@ func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
|
||||
procData.CpuPercent = fmt.Sprintf("%.2f", procData.CpuValue) + "%"
|
||||
menInfo, procErr := proc.MemoryInfo()
|
||||
if procErr == nil {
|
||||
procData.Rss = formatBytes(menInfo.RSS)
|
||||
procData.Rss = common.FormatBytes(menInfo.RSS)
|
||||
procData.RssValue = menInfo.RSS
|
||||
procData.Data = formatBytes(menInfo.Data)
|
||||
procData.VMS = formatBytes(menInfo.VMS)
|
||||
procData.HWM = formatBytes(menInfo.HWM)
|
||||
procData.Stack = formatBytes(menInfo.Stack)
|
||||
procData.Locked = formatBytes(menInfo.Locked)
|
||||
procData.Swap = formatBytes(menInfo.Swap)
|
||||
procData.Data = common.FormatBytes(menInfo.Data)
|
||||
procData.VMS = common.FormatBytes(menInfo.VMS)
|
||||
procData.HWM = common.FormatBytes(menInfo.HWM)
|
||||
procData.Stack = common.FormatBytes(menInfo.Stack)
|
||||
procData.Locked = common.FormatBytes(menInfo.Locked)
|
||||
procData.Swap = common.FormatBytes(menInfo.Swap)
|
||||
} else {
|
||||
procData.Rss = "--"
|
||||
procData.Data = "--"
|
||||
@ -250,8 +231,8 @@ func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
|
||||
}
|
||||
ioStat, procErr := proc.IOCounters()
|
||||
if procErr == nil {
|
||||
procData.DiskWrite = formatBytes(ioStat.WriteBytes)
|
||||
procData.DiskRead = formatBytes(ioStat.ReadBytes)
|
||||
procData.DiskWrite = common.FormatBytes(ioStat.WriteBytes)
|
||||
procData.DiskRead = common.FormatBytes(ioStat.ReadBytes)
|
||||
} else {
|
||||
procData.DiskWrite = "--"
|
||||
procData.DiskRead = "--"
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
@ -70,3 +71,20 @@ func GetImagePrefix() string {
|
||||
func IsUseCustomApp() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// alert
|
||||
func CreateAlert(createAlert dto.CreateOrUpdateAlert) error {
|
||||
return nil
|
||||
}
|
||||
func UpdateAlert(updateAlert dto.CreateOrUpdateAlert) error {
|
||||
return nil
|
||||
}
|
||||
func DeleteAlert(alertBase dto.AlertBase) error {
|
||||
return nil
|
||||
}
|
||||
func GetAlert(alertBase dto.AlertBase) uint {
|
||||
return 0
|
||||
}
|
||||
func PushAlert(pushAlert dto.PushAlert) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -382,3 +382,52 @@ func (b *BaseApi) ReloadSSL(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary generate api key
|
||||
// @Description 生成 API 接口密钥
|
||||
// @Accept json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/api/config/generate/key [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"}
|
||||
func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
||||
panelToken := c.GetHeader("1Panel-Token")
|
||||
if panelToken != "" {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil)
|
||||
return
|
||||
}
|
||||
apiKey, err := settingService.GenerateApiKey()
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, apiKey)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Update api config
|
||||
// @Description 更新 API 接口配置
|
||||
// @Accept json
|
||||
// @Param request body dto.ApiInterfaceConfig true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/api/config/update [post]
|
||||
// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"}
|
||||
func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
|
||||
panelToken := c.GetHeader("1Panel-Token")
|
||||
if panelToken != "" {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil)
|
||||
return
|
||||
}
|
||||
var req dto.ApiInterfaceConfig
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := settingService.UpdateApiConfig(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
@ -42,6 +42,10 @@ type SettingInfo struct {
|
||||
ProxyUser string `json:"proxyUser"`
|
||||
ProxyPasswd string `json:"proxyPasswd"`
|
||||
ProxyPasswdKeep string `json:"proxyPasswdKeep"`
|
||||
|
||||
ApiInterfaceStatus string `json:"apiInterfaceStatus"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
IpWhiteList string `json:"ipWhiteList"`
|
||||
}
|
||||
|
||||
type SettingUpdate struct {
|
||||
@ -196,6 +200,12 @@ type XpackHideMenu struct {
|
||||
Children []XpackHideMenu `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type ApiInterfaceConfig struct {
|
||||
ApiInterfaceStatus string `json:"apiInterfaceStatus"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
IpWhiteList string `json:"ipWhiteList"`
|
||||
}
|
||||
|
||||
type TerminalInfo struct {
|
||||
LineHeight string `json:"lineHeight"`
|
||||
LetterSpacing string `json:"letterSpacing"`
|
||||
|
@ -38,6 +38,8 @@ type ISettingService interface {
|
||||
UpdateSSL(c *gin.Context, req dto.SSLUpdate) error
|
||||
LoadFromCert() (*dto.SSLInfo, error)
|
||||
HandlePasswordExpired(c *gin.Context, old, new string) error
|
||||
GenerateApiKey() (string, error)
|
||||
UpdateApiConfig(req dto.ApiInterfaceConfig) error
|
||||
|
||||
GetTerminalInfo() (*dto.TerminalInfo, error)
|
||||
UpdateTerminal(req dto.TerminalInfo) error
|
||||
@ -410,6 +412,31 @@ func (u *SettingService) UpdateSystemSSL() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SettingService) GenerateApiKey() (string, error) {
|
||||
apiKey := common.RandStr(32)
|
||||
if err := settingRepo.Update("ApiKey", apiKey); err != nil {
|
||||
return global.CONF.System.ApiKey, err
|
||||
}
|
||||
global.CONF.System.ApiKey = apiKey
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error {
|
||||
if err := settingRepo.Update("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
global.CONF.System.ApiInterfaceStatus = req.ApiInterfaceStatus
|
||||
if err := settingRepo.Update("ApiKey", req.ApiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
global.CONF.System.ApiKey = req.ApiKey
|
||||
if err := settingRepo.Update("IpWhiteList", req.IpWhiteList); err != nil {
|
||||
return err
|
||||
}
|
||||
global.CONF.System.IpWhiteList = req.IpWhiteList
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadInfoFromCert() (dto.SSLInfo, error) {
|
||||
var info dto.SSLInfo
|
||||
certFile := path.Join(global.CONF.System.BaseDir, "1panel/secret/server.crt")
|
||||
|
@ -18,6 +18,25 @@ import (
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
// @host localhost
|
||||
// @BasePath /api/v2
|
||||
// @schemes http https
|
||||
|
||||
// @securityDefinitions.apikey CustomToken
|
||||
// @description 自定义 Token 格式,格式:md5('1panel' + 1Panel-Token + 1Panel-Timestamp)。
|
||||
// @description ```
|
||||
// @description 示例请求头:
|
||||
// @description curl -X GET "http://localhost:4004/api/v1/resource" \
|
||||
// @description -H "1Panel-Token: <1panel_token>" \
|
||||
// @description -H "1Panel-Timestamp: <current_unix_timestamp>"
|
||||
// @description ```
|
||||
// @description - `1Panel-Token` 为面板 API 接口密钥。
|
||||
// @type apiKey
|
||||
// @in Header
|
||||
// @name 1Panel-Token
|
||||
// @securityDefinitions.apikey Timestamp
|
||||
// @type apiKey
|
||||
// @in header
|
||||
// @name 1Panel-Timestamp
|
||||
// @description - `1Panel-Timestamp` 为当前时间的 Unix 时间戳(单位:秒)。
|
||||
|
||||
func main() {
|
||||
if err := cmd.RootCmd.Execute(); err != nil {
|
||||
|
@ -18,4 +18,8 @@ type System struct {
|
||||
Entrance string `mapstructure:"entrance"`
|
||||
IsDemo bool `mapstructure:"is_demo"`
|
||||
ChangeUserInfo string `mapstructure:"change_user_info"`
|
||||
|
||||
ApiInterfaceStatus string `mapstructure:"api_interface_status"`
|
||||
ApiKey string `mapstructure:"api_key"`
|
||||
IpWhiteList string `mapstructure:"ip_white_list"`
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ var (
|
||||
ErrInitialPassword = errors.New("ErrInitialPassword")
|
||||
ErrInvalidParams = errors.New("ErrInvalidParams")
|
||||
ErrNotSupportType = errors.New("ErrNotSupportType")
|
||||
ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid"
|
||||
ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid"
|
||||
ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid"
|
||||
ErrApiConfigDisable = "ErrApiConfigDisable"
|
||||
|
||||
ErrTokenParse = errors.New("ErrTokenParse")
|
||||
ErrStructTransform = errors.New("ErrStructTransform")
|
||||
|
@ -9,6 +9,10 @@ ErrNotLogin: "User is not Login: {{ .detail }}"
|
||||
ErrPasswordExpired: "The current password has expired: {{ .detail }}"
|
||||
ErrNotSupportType: "The system does not support the current type: {{ .detail }}"
|
||||
ErrProxy: "Request error, please check the node status: {{ .detail }}"
|
||||
ErrApiConfigStatusInvalid: "API Interface access prohibited: {{ .detail }}"
|
||||
ErrApiConfigKeyInvalid: "API Interface key error: {{ .detail }}"
|
||||
ErrApiConfigIPInvalid: "API Interface IP is not on the whitelist: {{ .detail }}"
|
||||
ErrApiConfigDisable: "This interface prohibits the use of API Interface calls: {{ .detail }}"
|
||||
|
||||
#common
|
||||
ErrNameIsExist: "Name is already exist"
|
||||
|
@ -9,6 +9,10 @@ ErrNotLogin: "用戶未登入: {{ .detail }}"
|
||||
ErrPasswordExpired: "當前密碼已過期: {{ .detail }}"
|
||||
ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}"
|
||||
ErrProxy: "請求錯誤,請檢查該節點狀態: {{ .detail }}"
|
||||
ErrApiConfigStatusInvalid: "API 接口禁止訪問: {{ .detail }}"
|
||||
ErrApiConfigKeyInvalid: "API 接口密钥錯誤: {{ .detail }}"
|
||||
ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}"
|
||||
ErrApiConfigDisable: "此接口禁止使用 API 接口調用: {{ .detail }}"
|
||||
|
||||
#common
|
||||
ErrNameIsExist: "名稱已存在"
|
||||
|
@ -9,6 +9,10 @@ ErrNotLogin: "用户未登录: {{ .detail }}"
|
||||
ErrPasswordExpired: "当前密码已过期: {{ .detail }}"
|
||||
ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}"
|
||||
ErrProxy: "请求错误,请检查该节点状态: {{ .detail }}"
|
||||
ErrApiConfigStatusInvalid: "API 接口禁止访问: {{ .detail }}"
|
||||
ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}"
|
||||
ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}"
|
||||
ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}"
|
||||
|
||||
#common
|
||||
ErrDemoEnvironment: "演示服务器,禁止此操作!"
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/core/constant"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/common"
|
||||
@ -22,6 +23,23 @@ func Init() {
|
||||
global.LOG.Errorf("load ipv6 status from setting failed, err: %v", err)
|
||||
}
|
||||
global.CONF.System.Ipv6 = ipv6Setting.Value
|
||||
apiInterfaceStatusSetting, err := settingRepo.Get(repo.WithByKey("ApiInterfaceStatus"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load service api interface from setting failed, err: %v", err)
|
||||
}
|
||||
global.CONF.System.ApiInterfaceStatus = apiInterfaceStatusSetting.Value
|
||||
if apiInterfaceStatusSetting.Value == constant.StatusEnable {
|
||||
apiKeySetting, err := settingRepo.Get(repo.WithByKey("ApiKey"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load service api key from setting failed, err: %v", err)
|
||||
}
|
||||
global.CONF.System.ApiKey = apiKeySetting.Value
|
||||
ipWhiteListSetting, err := settingRepo.Get(repo.WithByKey("IpWhiteList"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load service ip white list from setting failed, err: %v", err)
|
||||
}
|
||||
global.CONF.System.IpWhiteList = ipWhiteListSetting.Value
|
||||
}
|
||||
bindAddressSetting, err := settingRepo.Get(repo.WithByKey("BindAddress"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load bind address from setting failed, err: %v", err)
|
||||
|
@ -89,7 +89,8 @@ var InitSetting = &gormigrate.Migration{
|
||||
if err := tx.Create(&model.Setting{Key: "PrsoxyPasswdKeep", Value: ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: "{\"id\":\"1\",\"label\":\"/xpack\",\"isCheck\":true,\"title\":\"xpack.menu\",\"children\":[{\"id\":\"2\",\"title\":\"xpack.waf.name\",\"path\":\"/xpack/waf/dashboard\",\"label\":\"Dashboard\",\"isCheck\":true},{\"id\":\"3\",\"title\":\"xpack.tamper.tamper\",\"path\":\"/xpack/tamper\",\"label\":\"Tamper\",\"isCheck\":true},{\"id\":\"4\",\"title\":\"xpack.gpu.gpu\",\"path\":\"/xpack/gpu\",\"label\":\"GPU\",\"isCheck\":true},{\"id\":\"5\",\"title\":\"xpack.setting.setting\",\"path\":\"/xpack/setting\",\"label\":\"XSetting\",\"isCheck\":true},{\"id\":\"6\",\"title\":\"xpack.monitor.name\",\"path\":\"/xpack/monitor/dashboard\",\"label\":\"MonitorDashboard\",\"isCheck\":true},{\"id\":\"7\",\"title\":\"xpack.node.nodeManagement\",\"path\":\"/xpack/node\",\"label\":\"Node\",\"isCheck\":true}]}"}).Error; err != nil {
|
||||
val := `{"id":"1","label":"/xpack","isCheck":true,"title":"xpack.menu","children":[{"id":"2","label":"Dashboard","isCheck":true,"title":"xpack.waf.name","path":"/xpack/waf/dashboard"},{"id":"3","label":"Tamper","isCheck":true,"title":"xpack.tamper.tamper","path":"/xpack/tamper"},{"id":"4","label":"GPU","isCheck":true,"title":"xpack.gpu.gpu","path":"/xpack/gpu"},{"id":"5","label":"XSetting","isCheck":true,"title":"xpack.setting.setting","path":"/xpack/setting"},{"id":"6","label":"MonitorDashboard","isCheck":true,"title":"xpack.monitor.name","path":"/xpack/monitor/dashboard"},{"id":"7","label":"XAlertDashboard","isCheck":true,"title":"xpack.alert.alert","path":"/xpack/alert/dashboard"},{"id":"8","label":"Node","isCheck":true,"title":"xpack.node.nodeManagement","path":"/xpack/node"}]}`
|
||||
if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: val}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -144,6 +145,15 @@ var InitSetting = &gormigrate.Migration{
|
||||
if err := tx.Create(&model.Setting{Key: "NoAuthSetting", Value: "200"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.Setting{Key: "ApiInterfaceStatus", Value: "disable"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.Setting{Key: "ApiKey", Value: ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.Setting{Key: "IpWhiteList", Value: ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -21,6 +24,29 @@ func SessionAuth() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
panelToken := c.GetHeader("1Panel-Token")
|
||||
panelTimestamp := c.GetHeader("1Panel-Timestamp")
|
||||
if panelToken != "" || panelTimestamp != "" {
|
||||
if global.CONF.System.ApiInterfaceStatus == "enable" {
|
||||
clientIP := c.ClientIP()
|
||||
if !isValid1PanelToken(panelToken, panelTimestamp) {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !isIPInWhiteList(clientIP) {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigIPInvalid, nil)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
return
|
||||
} else {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigStatusInvalid, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
psession, err := global.SESSION.Get(c)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrTypeNotLogin, err)
|
||||
@ -42,3 +68,35 @@ func SessionAuth() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func isValid1PanelToken(panelToken string, panelTimestamp string) bool {
|
||||
system1PanelToken := global.CONF.System.ApiKey
|
||||
return GenerateMD5("1panel"+panelToken+panelTimestamp) == GenerateMD5("1panel"+system1PanelToken+panelTimestamp)
|
||||
}
|
||||
|
||||
func isIPInWhiteList(clientIP string) bool {
|
||||
ipWhiteString := global.CONF.System.IpWhiteList
|
||||
ipWhiteList := strings.Split(ipWhiteString, "\n")
|
||||
for _, cidr := range ipWhiteList {
|
||||
if cidr == "0.0.0.0" {
|
||||
return true
|
||||
}
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
if cidr == clientIP {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ipNet.Contains(net.ParseIP(clientIP)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GenerateMD5(input string) string {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(input))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
settingRouter.POST("/upgrade", baseApi.Upgrade)
|
||||
settingRouter.POST("/upgrade/notes", baseApi.GetNotesByVersion)
|
||||
settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo)
|
||||
settingRouter.POST("/api/config/generate/key", baseApi.GenerateApiKey)
|
||||
settingRouter.POST("/api/config/update", baseApi.UpdateApiConfig)
|
||||
|
||||
noAuthRouter.POST("/ssl/reload", baseApi.ReloadSSL)
|
||||
}
|
||||
|
@ -34,6 +34,9 @@ export namespace Cronjob {
|
||||
retainCopies: number;
|
||||
status: string;
|
||||
secret: string;
|
||||
hasAlert: boolean;
|
||||
alertCount: number;
|
||||
alertTitle: string;
|
||||
}
|
||||
export interface Item {
|
||||
val: string;
|
||||
|
@ -11,7 +11,7 @@ export namespace File {
|
||||
size: number;
|
||||
isDir: boolean;
|
||||
isSymlink: boolean;
|
||||
linkPath: boolean;
|
||||
linkPath: string;
|
||||
type: string;
|
||||
updateTime: string;
|
||||
modTime: string;
|
||||
|
@ -57,6 +57,10 @@ export namespace Setting {
|
||||
proxyUser: string;
|
||||
proxyPasswd: string;
|
||||
proxyPasswdKeep: string;
|
||||
|
||||
apiInterfaceStatus: string;
|
||||
apiKey: string;
|
||||
ipWhiteList: string;
|
||||
}
|
||||
export interface TerminalInfo {
|
||||
lineHeight: string;
|
||||
@ -79,6 +83,11 @@ export namespace Setting {
|
||||
proxyPasswd: string;
|
||||
proxyPasswdKeep: string;
|
||||
}
|
||||
export interface ApiConfig {
|
||||
apiInterfaceStatus: string;
|
||||
apiKey: string;
|
||||
ipWhiteList: string;
|
||||
}
|
||||
export interface SSLUpdate {
|
||||
ssl: string;
|
||||
domain: string;
|
||||
@ -211,6 +220,8 @@ export namespace Setting {
|
||||
trial: boolean;
|
||||
status: string;
|
||||
message: string;
|
||||
smsUsed: number;
|
||||
smsTotal: number;
|
||||
}
|
||||
export interface LicenseStatus {
|
||||
productPro: string;
|
||||
|
@ -139,6 +139,9 @@ export namespace Toolbox {
|
||||
spec: string;
|
||||
specObj: Cronjob.SpecObj;
|
||||
description: string;
|
||||
hasAlert: boolean;
|
||||
alertCount: number;
|
||||
alertTitle: string;
|
||||
}
|
||||
export interface ClamCreate {
|
||||
name: string;
|
||||
|
@ -74,7 +74,7 @@ export const WgetFile = (params: File.FileWget) => {
|
||||
};
|
||||
|
||||
export const MoveFile = (params: File.FileMove) => {
|
||||
return http.post<File.File>('files/move', params);
|
||||
return http.post<File.File>('files/move', params, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const DownloadFile = (params: File.FileDownload) => {
|
||||
|
@ -150,3 +150,11 @@ export const loadReleaseNotes = (version: string) => {
|
||||
export const upgrade = (version: string) => {
|
||||
return http.post(`/core/settings/upgrade`, { version: version });
|
||||
};
|
||||
|
||||
// api config
|
||||
export const generateApiKey = () => {
|
||||
return http.post<string>(`/core/settings/api/config/generate/key`);
|
||||
};
|
||||
export const updateApiConfig = (param: Setting.ApiConfig) => {
|
||||
return http.post(`/core/settings/api/config/update`, param);
|
||||
};
|
||||
|
@ -141,7 +141,7 @@ const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const paths = ref<string[]>([]);
|
||||
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
|
||||
const selectRow = ref();
|
||||
const selectRow = ref({ path: '', name: '' });
|
||||
const rowRefs = ref();
|
||||
const popoverVisible = ref(false);
|
||||
const newFolder = ref();
|
||||
@ -183,12 +183,12 @@ const selectFile = () => {
|
||||
|
||||
const closePage = () => {
|
||||
popoverVisible.value = false;
|
||||
selectRow.value = {};
|
||||
selectRow.value = { path: '', name: '' };
|
||||
};
|
||||
|
||||
const openPage = () => {
|
||||
popoverVisible.value = true;
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? props.path || '/' : '';
|
||||
rowName.value = '';
|
||||
};
|
||||
|
||||
@ -216,7 +216,7 @@ const open = async (row: File.File) => {
|
||||
}
|
||||
await search(req);
|
||||
}
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? req.path : '';
|
||||
rowName.value = '';
|
||||
};
|
||||
|
||||
@ -230,7 +230,7 @@ const jump = async (index: number) => {
|
||||
}
|
||||
path = path || '/';
|
||||
req.path = path;
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? req.path : '';
|
||||
rowName.value = '';
|
||||
await search(req);
|
||||
popoverVisible.value = true;
|
||||
@ -286,7 +286,7 @@ const cancelFolder = (row: any) => {
|
||||
data.value.shift();
|
||||
row.isCreate = false;
|
||||
disBtn.value = false;
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? req.path : '';
|
||||
rowName.value = '';
|
||||
newFolder.value = '';
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ import { UploadFileData } from '@/api/modules/setting';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
|
||||
import { useTheme } from '@/global/use-theme';
|
||||
import { getXpackSetting } from '@/utils/xpack';
|
||||
import { getXpackSetting, initFavicon } from '@/utils/xpack';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const { switchTheme } = useTheme();
|
||||
@ -90,10 +90,12 @@ const submit = async () => {
|
||||
globalStore.isProductPro = true;
|
||||
const xpackRes = await getXpackSetting();
|
||||
if (xpackRes) {
|
||||
globalStore.themeConfig.isGold = xpackRes.data.theme === 'dark-gold';
|
||||
globalStore.themeConfig.theme = xpackRes.data.theme;
|
||||
globalStore.themeConfig.themeColor = xpackRes.data.themeColor;
|
||||
}
|
||||
loading.value = false;
|
||||
switchTheme();
|
||||
initFavicon();
|
||||
uploadRef.value!.clearFiles();
|
||||
uploaderFiles.value = [];
|
||||
open.value = false;
|
||||
|
@ -322,7 +322,7 @@ defineExpose({ changeTail, onDownload, clearLog });
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
background-color: #1e1e1e;
|
||||
background-color: var(--panel-logs-bg-color);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
@ -94,8 +94,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: $primary-color;
|
||||
border-color: $primary-color !important;
|
||||
color: var(--panel-button-text-color) !important;
|
||||
background-color: var(--panel-button-bg-color) !important;
|
||||
border-color: var(--panel-color-primary) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
@ -2,39 +2,39 @@
|
||||
<div>
|
||||
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
|
||||
<div class="flex flex-wrap items-center" v-if="props.footer">
|
||||
<el-button type="primary" link @click="toForum">
|
||||
<el-link type="primary" :underline="false" @click="toForum">
|
||||
<span class="font-normal">{{ $t('setting.forum') }}</span>
|
||||
</el-button>
|
||||
</el-link>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="primary" link @click="toDoc">
|
||||
<el-link type="primary" :underline="false" @click="toDoc">
|
||||
<span class="font-normal">{{ $t('setting.doc2') }}</span>
|
||||
</el-button>
|
||||
</el-link>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="primary" link @click="toGithub">
|
||||
<el-link type="primary" :underline="false" @click="toGithub">
|
||||
<span class="font-normal">{{ $t('setting.project') }}</span>
|
||||
</el-button>
|
||||
</el-link>
|
||||
<el-divider direction="vertical" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<el-button type="primary" link @click="toHalo">
|
||||
<span class="font-normal">
|
||||
<el-link :underline="false" type="primary" @click="toHalo">
|
||||
{{ isMasterProductPro ? $t('license.pro') : $t('license.community') }}
|
||||
</span>
|
||||
</el-button>
|
||||
<span class="version" @click="copyText(version)">{{ version }}</span>
|
||||
<el-badge is-dot style="margin-top: -3px" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||
<el-button type="primary" link @click="onLoadUpgradeInfo">
|
||||
<span class="font-normal">({{ $t('setting.hasNewVersion') }})</span>
|
||||
</el-button>
|
||||
</el-link>
|
||||
<el-link :underline="false" class="version" type="primary" @click="copyText(version)">
|
||||
{{ version }}
|
||||
</el-link>
|
||||
<el-badge is-dot class="-mt-0.5" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||
<el-link :underline="false" type="primary" @click="onLoadUpgradeInfo">
|
||||
({{ $t('setting.hasNewVersion') }})
|
||||
</el-link>
|
||||
</el-badge>
|
||||
<el-button
|
||||
<el-link
|
||||
v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
|
||||
type="primary"
|
||||
link
|
||||
:underline="false"
|
||||
@click="onLoadUpgradeInfo"
|
||||
>
|
||||
<span>({{ $t('setting.upgradeCheck') }})</span>
|
||||
</el-button>
|
||||
({{ $t('setting.upgradeCheck') }})
|
||||
</el-link>
|
||||
<el-tag v-if="version === 'Waiting'" round style="margin-left: 10px">
|
||||
{{ $t('setting.upgrading') }}
|
||||
</el-tag>
|
||||
@ -129,7 +129,7 @@ onMounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.version {
|
||||
font-size: 14px;
|
||||
color: var(--dark-gold-base-color);
|
||||
color: var(--panel-color-primary-light-4);
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
@ -7,11 +7,6 @@
|
||||
:key="refresh"
|
||||
>
|
||||
<div class="panel-MdEditor">
|
||||
<el-alert :closable="false">
|
||||
<span class="line-height">{{ $t('setting.versionHelper') }}</span>
|
||||
<li class="line-height">{{ $t('setting.versionHelper1') }}</li>
|
||||
<li class="line-height">{{ $t('setting.versionHelper2') }}</li>
|
||||
</el-alert>
|
||||
<div class="default-theme" style="margin-left: 20px">
|
||||
<h2 class="inline-block">{{ $t('app.version') }}</h2>
|
||||
</div>
|
||||
@ -113,10 +108,16 @@ defineExpose({
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.default-theme h2) {
|
||||
color: var(--dark-gold-base-color);
|
||||
margin: 13px, 0;
|
||||
color: var(--el-color-primary);
|
||||
margin: 13px 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.el-link__inner) {
|
||||
font-weight: 400;
|
||||
}
|
||||
:deep(.md-editor-dark) {
|
||||
background-color: var(--panel-main-bg-color-9);
|
||||
}
|
||||
</style>
|
||||
|
@ -75,12 +75,13 @@ const acceptParams = (props: WsProps) => {
|
||||
};
|
||||
|
||||
const newTerm = () => {
|
||||
const background = getComputedStyle(document.documentElement).getPropertyValue('--panel-terminal-bg-color').trim();
|
||||
term.value = new Terminal({
|
||||
lineHeight: 1.2,
|
||||
fontSize: 12,
|
||||
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
||||
theme: {
|
||||
background: '#000000',
|
||||
background: background,
|
||||
},
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'underline',
|
||||
@ -251,4 +252,7 @@ onBeforeUnmount(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.xterm) {
|
||||
padding: 5px !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -30,10 +30,24 @@ const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}, // option: { title , xData, yData, formatStr, yAxis, grid, tooltip}
|
||||
},
|
||||
});
|
||||
|
||||
const seriesStyle = [
|
||||
{
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim(),
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--panel-color-primary-light-9')
|
||||
.trim(),
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
@ -98,6 +112,7 @@ function initChart() {
|
||||
series.push({
|
||||
name: item?.name,
|
||||
type: 'line',
|
||||
itemStyle: seriesStyle[index + 2],
|
||||
areaStyle: seriesStyle[index],
|
||||
data: item?.data,
|
||||
showSymbol: false,
|
||||
|
@ -7,7 +7,7 @@ import * as echarts from 'echarts';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
const globalStore = GlobalStore();
|
||||
const { isDarkGoldTheme, isDarkTheme } = storeToRefs(globalStore);
|
||||
const { isDarkTheme } = storeToRefs(globalStore);
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@ -25,7 +25,7 @@ const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}, // option: { title , data }
|
||||
},
|
||||
});
|
||||
|
||||
function initChart() {
|
||||
@ -34,6 +34,12 @@ function initChart() {
|
||||
myChart = echarts.init(document.getElementById(props.id) as HTMLElement);
|
||||
}
|
||||
let percentText = String(props.option.data).split('.');
|
||||
const primaryLight2 = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--panel-color-primary-light-3')
|
||||
.trim();
|
||||
const primaryLight1 = getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim();
|
||||
const pieBgColor = getComputedStyle(document.documentElement).getPropertyValue('--panel-pie-bg-color').trim();
|
||||
|
||||
const option = {
|
||||
title: [
|
||||
{
|
||||
@ -99,11 +105,11 @@ function initChart() {
|
||||
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: isDarkGoldTheme.value ? '#836c4c' : 'rgba(81, 192, 255, .1)',
|
||||
color: primaryLight2,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: isDarkGoldTheme.value ? '#eaba63' : '#4261F6',
|
||||
color: primaryLight1,
|
||||
},
|
||||
]),
|
||||
],
|
||||
@ -119,7 +125,7 @@ function initChart() {
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
color: isDarkTheme.value ? '#16191D' : '#fff',
|
||||
color: pieBgColor,
|
||||
data: [
|
||||
{
|
||||
value: 0,
|
||||
|
@ -557,6 +557,19 @@ const checkHttpOrHttps = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkPhone = (rule: any, value: any, callback: any) => {
|
||||
if (value === '' || typeof value === 'undefined' || value == null) {
|
||||
callback();
|
||||
} else {
|
||||
const reg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/;
|
||||
if (!reg.test(value) && value !== '') {
|
||||
callback(new Error(i18n.global.t('commons.rule.phone')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface CommonRule {
|
||||
requiredInput: FormItemRule;
|
||||
requiredSelect: FormItemRule;
|
||||
@ -602,6 +615,7 @@ interface CommonRule {
|
||||
paramExtUrl: FormItemRule;
|
||||
paramSimple: FormItemRule;
|
||||
paramHttp: FormItemRule;
|
||||
phone: FormItemRule;
|
||||
}
|
||||
|
||||
export const Rules: CommonRule = {
|
||||
@ -828,4 +842,9 @@ export const Rules: CommonRule = {
|
||||
validator: checkDomainOrIP,
|
||||
trigger: 'blur',
|
||||
},
|
||||
phone: {
|
||||
validator: checkPhone,
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
},
|
||||
};
|
||||
|
20
frontend/src/global/use-logo.ts
Normal file
20
frontend/src/global/use-logo.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { GlobalStore } from '@/store';
|
||||
import { getXpackSetting } from '@/utils/xpack';
|
||||
|
||||
export const useLogo = async () => {
|
||||
const globalStore = GlobalStore();
|
||||
const res = await getXpackSetting();
|
||||
if (res) {
|
||||
localStorage.setItem('1p-favicon', res.data.logo);
|
||||
globalStore.themeConfig.title = res.data.title;
|
||||
globalStore.themeConfig.logo = res.data.logo;
|
||||
globalStore.themeConfig.logoWithText = res.data.logoWithText;
|
||||
globalStore.themeConfig.favicon = res.data.favicon;
|
||||
}
|
||||
|
||||
const link = (document.querySelector("link[rel*='icon']") || document.createElement('link')) as HTMLLinkElement;
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = globalStore.themeConfig.favicon ? `/api/v1/images/favicon?t=${Date.now()}` : '/public/favicon.png';
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
};
|
@ -1,22 +1,29 @@
|
||||
import { GlobalStore } from '@/store';
|
||||
import { setPrimaryColor } from '@/utils/theme';
|
||||
|
||||
export const useTheme = () => {
|
||||
const globalStore = GlobalStore();
|
||||
const switchTheme = () => {
|
||||
if (globalStore.themeConfig.isGold && globalStore.isMasterProductPro) {
|
||||
const body = document.documentElement as HTMLElement;
|
||||
body.setAttribute('class', 'dark-gold');
|
||||
return;
|
||||
const globalStore = GlobalStore();
|
||||
const themeConfig = globalStore.themeConfig;
|
||||
let itemTheme = themeConfig.theme;
|
||||
if (itemTheme === 'auto') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
itemTheme = prefersDark ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.className = itemTheme === 'dark' ? 'dark' : 'light';
|
||||
if (globalStore.isProductPro && themeConfig.themeColor) {
|
||||
try {
|
||||
const themeColor = JSON.parse(themeConfig.themeColor);
|
||||
const color = itemTheme === 'dark' ? themeColor.dark : themeColor.light;
|
||||
|
||||
let itemTheme = globalStore.themeConfig.theme;
|
||||
if (globalStore.themeConfig.theme === 'auto') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
itemTheme = prefersDark.matches ? 'dark' : 'light';
|
||||
if (color) {
|
||||
themeConfig.primary = color;
|
||||
setPrimaryColor(color);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse themeColor', e);
|
||||
}
|
||||
}
|
||||
const body = document.documentElement as HTMLElement;
|
||||
if (itemTheme === 'dark') body.setAttribute('class', 'dark');
|
||||
else body.setAttribute('class', '');
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -235,8 +235,7 @@ const message = {
|
||||
formatErr: 'Format error, please check and retry',
|
||||
phpExtension: 'Only supports , _ lowercase English and numbers',
|
||||
paramHttp: 'Must start with http:// or https://',
|
||||
diffHelper:
|
||||
'The left side is the old version, the right side is the new version, after editing, click Save using custom version',
|
||||
phone: 'The format of the phone number is incorrect',
|
||||
},
|
||||
res: {
|
||||
paramError: 'The request failed, please try again later!',
|
||||
@ -981,6 +980,7 @@ const message = {
|
||||
|
||||
requestExpirationTime: 'Upload Request Expiration Time(Hours)',
|
||||
unitHours: 'Unit: Hours',
|
||||
alertTitle: 'Planned Task - {0} 「{1}」 Task Failure Alert',
|
||||
},
|
||||
monitor: {
|
||||
monitor: 'Monitor',
|
||||
@ -1195,6 +1195,8 @@ const message = {
|
||||
clamLog: 'Scan Logs',
|
||||
freshClam: 'Update Virus Definitions',
|
||||
freshClamLog: 'Update Virus Definitions Logs',
|
||||
alertHelper: 'Professional version supports scheduled scan and SMS alert',
|
||||
alertTitle: 'Virus scan task 「{0}」 detected infected file alert',
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
@ -1233,6 +1235,14 @@ const message = {
|
||||
taskName: 'Task Name',
|
||||
taskRunning: 'Running',
|
||||
},
|
||||
alert: {
|
||||
isAlert: 'Alert',
|
||||
alertCount: 'Alert Count',
|
||||
clamHelper: 'Trigger SMS alert when scanning infected files',
|
||||
cronJobHelper: 'Trigger SMS alert when scheduled task execution fails',
|
||||
licenseHelper: 'Professional version supports SMS alert',
|
||||
alertCountHelper: 'Maximum daily alarm frequency',
|
||||
},
|
||||
file: {
|
||||
dir: 'Folder',
|
||||
upload: 'Upload',
|
||||
@ -1353,6 +1363,8 @@ const message = {
|
||||
noNameFolder: 'Untitled Folder',
|
||||
noNameFile: 'Untitled File',
|
||||
minimap: 'Code Mini Map',
|
||||
fileCanNotRead: 'File can not read',
|
||||
panelInstallDir: '1Panel installation directory cannot be deleted',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Auto Start',
|
||||
@ -1449,10 +1461,34 @@ const message = {
|
||||
proxyHelper1: 'Downloading and synchronizing installation packages from the app store (Professional)',
|
||||
proxyHelper2: 'System version upgrades and retrieving update information (Professional)',
|
||||
proxyHelper3: 'Verification and synchronization of system licenses',
|
||||
proxyHelper4: 'Docker network access will be done through a proxy server (Professional)',
|
||||
proxyType: 'Proxy Type',
|
||||
proxyUrl: 'Proxy Address',
|
||||
proxyPort: 'Proxy Port',
|
||||
proxyPasswdKeep: 'Remember Password',
|
||||
proxyDocker: 'Docker Proxy',
|
||||
proxyDockerHelper:
|
||||
'Synchronize proxy server configuration to Docker, support offline server image pulling and other operations',
|
||||
apiInterface: 'API Interface',
|
||||
apiInterfaceClose: 'Once closed, API interfaces cannot be accessed. Do you want to continue?',
|
||||
apiInterfaceHelper: 'Provide panel support for API interface access',
|
||||
apiInterfaceAlert1:
|
||||
'Please do not enable it in production environments as it may increase server security risks',
|
||||
apiInterfaceAlert2:
|
||||
'Please do not use third-party applications to call the panel API to prevent potential security threats.',
|
||||
apiInterfaceAlert3: 'API Interface Document:',
|
||||
apiInterfaceAlert4: 'Usage Document:',
|
||||
apiKey: 'Interface Key',
|
||||
apiKeyHelper: 'Interface key is used for external applications to access API interfaces',
|
||||
ipWhiteList: 'IP Whitelist',
|
||||
ipWhiteListEgs:
|
||||
'When there are multiple IPs, line breaks are required for display, for example: \n172.161.10.111 \n172.161.10.0/24 ',
|
||||
ipWhiteListHelper: 'IPs must be in the IP whitelist list to access the panel API interface',
|
||||
apiKeyReset: 'Interface key reset',
|
||||
apiKeyResetHelper: 'the associated key service will become invalid. Please add a new key to the service',
|
||||
confDockerProxy: 'Configure Docker Proxy',
|
||||
restartNowHelper: 'Configuring Docker proxy requires restarting the Docker service.',
|
||||
restartNow: 'Restart immediately',
|
||||
systemIPWarning: 'The server address is not currently set. Please set it in the control panel first!',
|
||||
systemIPWarning1: 'The current server address is set to {0}, and quick redirection is not possible!',
|
||||
syncTime: 'Server Time',
|
||||
@ -1821,6 +1857,7 @@ const message = {
|
||||
'Upgrading to the professional version allows customization of panel logo, welcome message, and other information.',
|
||||
monitor:
|
||||
'Upgrade to the professional version to view the real-time status of the website, visitor trends, visitor sources, request logs and other information. ',
|
||||
alert: 'Upgrade to the professional version to receive alarm information via SMS and view alarm logs, fully control various key events, and ensure worry-free system operation',
|
||||
node: 'Upgrading to the professional version allows you to manage multiple Linux servers with 1Panel.',
|
||||
},
|
||||
clean: {
|
||||
|
@ -232,6 +232,7 @@ const message = {
|
||||
formatErr: '格式錯誤,檢查後重試',
|
||||
phpExtension: '僅支持 , _ 小寫英文和數字',
|
||||
paramHttp: '必須以 http:// 或 https:// 開頭',
|
||||
phone: '手機號碼格式不正確',
|
||||
},
|
||||
res: {
|
||||
paramError: '請求失敗,請稍後重試!',
|
||||
@ -934,6 +935,7 @@ const message = {
|
||||
|
||||
requestExpirationTime: '上傳請求過期時間(小時)',
|
||||
unitHours: '單位:小時',
|
||||
alertTitle: '計畫任務-{0}「{1}」任務失敗告警',
|
||||
},
|
||||
monitor: {
|
||||
monitor: '監控',
|
||||
@ -1131,6 +1133,8 @@ const message = {
|
||||
clamLog: '掃描日誌',
|
||||
freshClam: '病毒庫刷新配置',
|
||||
freshClamLog: '病毒庫刷新日誌',
|
||||
alertHelper: '專業版支持定時掃描和短信告警功能',
|
||||
alertTitle: '病毒掃描「{0}」任務检测到感染文件告警',
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
@ -1169,6 +1173,14 @@ const message = {
|
||||
taskName: '任務名稱',
|
||||
taskRunning: '運行中',
|
||||
},
|
||||
alert: {
|
||||
isAlert: '是否告警',
|
||||
alertCount: '告警次數',
|
||||
clamHelper: '掃描到感染檔案時觸發簡訊告警',
|
||||
cronJobHelper: '定時任務執行失敗時將觸發簡訊告警',
|
||||
licenseHelper: '專業版支持簡訊告警功能',
|
||||
alertCountHelper: '每日最大告警次數',
|
||||
},
|
||||
file: {
|
||||
dir: '文件夾',
|
||||
upload: '上傳',
|
||||
@ -1285,6 +1297,8 @@ const message = {
|
||||
noNameFolder: '未命名資料夾',
|
||||
noNameFile: '未命名檔案',
|
||||
minimap: '縮略圖',
|
||||
fileCanNotRead: '此文件不支持預覽',
|
||||
panelInstallDir: '1Panel 安裝目錄不能删除',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '開機自啟',
|
||||
@ -1373,10 +1387,30 @@ const message = {
|
||||
proxyHelper1: '應用商店的安裝包下載和同步(專業版功能)',
|
||||
proxyHelper2: '系統版本升級及獲取更新說明(專業版功能)',
|
||||
proxyHelper3: '系統許可證的驗證和同步',
|
||||
proxyHelper4: 'Docker 的網絡訪問將通過代理伺服器進行(專業版功能)',
|
||||
proxyType: '代理類型',
|
||||
proxyUrl: '代理地址',
|
||||
proxyPort: '代理端口',
|
||||
proxyPasswdKeep: '記住密碼',
|
||||
proxyDocker: 'Docker 代理',
|
||||
proxyDockerHelper: '將代理伺服器配寘同步至 Docker,支持離線服務器拉取鏡像等操作',
|
||||
apiInterface: 'API 接口',
|
||||
apiInterfaceClose: '關閉後將不能使用 API 接口進行訪問,是否繼續?',
|
||||
apiInterfaceHelper: '提供面板支持 API 接口訪問',
|
||||
apiInterfaceAlert1: '請不要在生產環境開啟,這可能新增服務器安全風險',
|
||||
apiInterfaceAlert2: '請不要使用協力廠商應用調用面板 API,以防止潜在的安全威脅。',
|
||||
apiInterfaceAlert3: 'API 接口檔案:',
|
||||
apiInterfaceAlert4: '使用檔案:',
|
||||
apiKey: '接口密钥',
|
||||
apiKeyHelper: '接口密钥用於外部應用訪問 API 接口',
|
||||
ipWhiteList: 'IP白名單',
|
||||
ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24',
|
||||
ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口',
|
||||
apiKeyReset: '接口密钥重置',
|
||||
apiKeyResetHelper: '重置密钥後,已關聯密钥服務將失效,請重新添加新密鑰至服務。',
|
||||
confDockerProxy: '配寘 Docker 代理',
|
||||
restartNowHelper: '配寘 Docker 代理需要重啓 Docker 服務。',
|
||||
restartNow: '立即重啓',
|
||||
systemIPWarning: '當前未設置服務器地址,請先在面板設置中設置!',
|
||||
systemIPWarning1: '當前服務器地址設置為 {0},無法快速跳轉!',
|
||||
changePassword: '密碼修改',
|
||||
@ -1691,6 +1725,7 @@ const message = {
|
||||
gpu: '升級專業版可以幫助用戶實時直觀查看到 GPU 的工作負載、溫度、顯存等重要參數。',
|
||||
setting: '升級專業版可以自定義面板 Logo、歡迎簡介等信息。',
|
||||
monitor: '升級專業版可以查看網站的即時狀態、訪客趨勢、訪客來源、請求日誌等資訊。 ',
|
||||
alert: '陞級專業版可通過簡訊接收告警資訊,並查看告警日誌,全面掌控各類關鍵事件,確保系統運行無憂。',
|
||||
node: '升級專業版可以使用 1Panel 管理多台 linux 伺服器。',
|
||||
},
|
||||
clean: {
|
||||
@ -2274,7 +2309,7 @@ const message = {
|
||||
domainHelper: '一行一個網域名稱,支援*和IP位址',
|
||||
pushDir: '推送憑證到本機目錄',
|
||||
dir: '目錄',
|
||||
pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem',
|
||||
pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 密钥檔案:privkey.pem',
|
||||
organizationDetail: '機構詳情',
|
||||
fromWebsite: '從網站獲取',
|
||||
dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值',
|
||||
|
@ -232,6 +232,7 @@ const message = {
|
||||
formatErr: '格式错误,检查后重试',
|
||||
phpExtension: '仅支持 , _ 小写英文和数字',
|
||||
paramHttp: '必须以 http:// 或 https:// 开头',
|
||||
phone: '手机号码格式不正确',
|
||||
},
|
||||
res: {
|
||||
paramError: '请求失败,请稍后重试!',
|
||||
@ -934,6 +935,7 @@ const message = {
|
||||
|
||||
requestExpirationTime: '上传请求过期时间(小时)',
|
||||
unitHours: '单位:小时',
|
||||
alertTitle: '计划任务-{0}「 {1} 」任务失败告警',
|
||||
},
|
||||
monitor: {
|
||||
monitor: '监控',
|
||||
@ -1132,6 +1134,8 @@ const message = {
|
||||
clamLog: '扫描日志',
|
||||
freshClam: '病毒库刷新配置',
|
||||
freshClamLog: '病毒库刷新日志',
|
||||
alertHelper: '专业版支持定时扫描和短信告警功能',
|
||||
alertTitle: '病毒扫描「 {0} 」任务检测到感染文件告警',
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
@ -1170,6 +1174,14 @@ const message = {
|
||||
taskName: '任务名称',
|
||||
taskRunning: '执行中',
|
||||
},
|
||||
alert: {
|
||||
isAlert: '是否告警',
|
||||
alertCount: '告警次数',
|
||||
clamHelper: '扫描到感染文件时触发短信告警',
|
||||
cronJobHelper: '定时任务执行失败时将触发短信告警',
|
||||
licenseHelper: '专业版支持短信告警功能',
|
||||
alertCountHelper: '每日最大告警次数',
|
||||
},
|
||||
file: {
|
||||
dir: '文件夹',
|
||||
upload: '上传',
|
||||
@ -1286,6 +1298,8 @@ const message = {
|
||||
noNameFolder: '未命名文件夹',
|
||||
noNameFile: '未命名文件',
|
||||
minimap: '缩略图',
|
||||
fileCanNotRead: '此文件不支持预览',
|
||||
panelInstallDir: '1Panel 安装目录不能删除',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '开机自启',
|
||||
@ -1374,10 +1388,29 @@ const message = {
|
||||
proxyHelper1: '应用商店的安装包下载和同步(专业版功能)',
|
||||
proxyHelper2: '系统版本升级及获取更新说明(专业版功能)',
|
||||
proxyHelper3: '系统许可证的验证和同步',
|
||||
proxyHelper4: 'Docker 的网络访问将通过代理服务器进行(专业版功能)',
|
||||
proxyType: '代理类型',
|
||||
proxyUrl: '代理地址',
|
||||
proxyPort: '代理端口',
|
||||
proxyPasswdKeep: '记住密码',
|
||||
proxyDocker: 'Docker 代理',
|
||||
apiInterface: 'API 接口',
|
||||
apiInterfaceClose: '关闭后将不能使用 API 接口进行访问,是否继续?',
|
||||
apiInterfaceHelper: '提供面板支持 API 接口访问',
|
||||
apiInterfaceAlert1: '请不要在生产环境开启,这可能增加服务器安全风险',
|
||||
apiInterfaceAlert2: '请不要使用第三方应用调用面板 API,以防止潜在的安全威胁。',
|
||||
apiInterfaceAlert3: 'API 接口文档:',
|
||||
apiInterfaceAlert4: '使用文档:',
|
||||
apiKey: '接口密钥',
|
||||
apiKeyHelper: '接口密钥用于外部应用访问 API 接口',
|
||||
ipWhiteList: 'IP 白名单',
|
||||
ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24',
|
||||
ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口',
|
||||
apiKeyReset: '接口密钥重置',
|
||||
apiKeyResetHelper: '重置密钥后,已关联密钥服务将失效,请重新添加新密钥至服务。',
|
||||
confDockerProxy: '配置 Docker 代理',
|
||||
restartNowHelper: '配置 Docker 代理需要重启 Docker 服务。',
|
||||
restartNow: '立即重启',
|
||||
systemIPWarning: '当前未设置服务器地址,请先在面板设置中设置!',
|
||||
systemIPWarning1: '当前服务器地址设置为 {0},无法快速跳转!',
|
||||
changePassword: '密码修改',
|
||||
@ -1691,6 +1724,7 @@ const message = {
|
||||
gpu: '升级专业版可以帮助用户实时直观查看到 GPU 的工作负载、温度、显存等重要参数。',
|
||||
setting: '升级专业版可以自定义面板 Logo、欢迎简介等信息。',
|
||||
monitor: '升级专业版可以查看网站的实时状态、访客趋势、访客来源、请求日志等信息。',
|
||||
alert: '升级专业版可通过短信接收告警信息,并查看告警日志,全面掌控各类关键事件,确保系统运行无忧。',
|
||||
node: '升级专业版可以使用 1Panel 管理多台 linux 服务器。',
|
||||
},
|
||||
clean: {
|
||||
|
@ -18,11 +18,12 @@ const isCollapse = computed(() => menuStore.isCollapse);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
border-top: 1px solid var(--panel-footer-border);
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
color: var(--panel-main-bg-color-1);
|
||||
margin-left: 25px;
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
|
@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="logo" style="cursor: pointer" @click="goHome">
|
||||
<template v-if="isCollapse">
|
||||
<img v-if="globalStore.themeConfig.logo" :src="'/api/v2/images/logo'" style="cursor: pointer" alt="logo" />
|
||||
<img
|
||||
v-if="globalStore.themeConfig.logo"
|
||||
:src="`/api/v2/images/logo?t=${Date.now()}`"
|
||||
style="cursor: pointer"
|
||||
alt="logo"
|
||||
/>
|
||||
<MenuLogo v-else />
|
||||
</template>
|
||||
<template v-else>
|
||||
<img
|
||||
v-if="globalStore.themeConfig.logoWithText"
|
||||
:src="'/api/v2/images/logoWithText'"
|
||||
:src="`/api/v2/images/logoWithText?t=${Date.now()}`"
|
||||
style="cursor: pointer"
|
||||
alt="logo"
|
||||
/>
|
||||
@ -37,6 +42,7 @@ const goHome = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 55px;
|
||||
z-index: 1;
|
||||
img {
|
||||
object-fit: contain;
|
||||
width: 95%;
|
||||
|
@ -6,6 +6,9 @@
|
||||
element-loading-svg-view-box="-10, -10, 50, 50"
|
||||
element-loading-background="rgba(122, 122, 122, 0.01)"
|
||||
>
|
||||
<div class="fixed">
|
||||
<PrimaryMenu />
|
||||
</div>
|
||||
<Logo :isCollapse="isCollapse" />
|
||||
<div class="el-dropdown-link flex justify-between items-center">
|
||||
<el-button link class="ml-4" @click="openChangeNode" @mouseenter="openChangeNode">
|
||||
@ -79,6 +82,7 @@ import { getSettingInfo, listNodeOptions } from '@/api/modules/setting';
|
||||
import { countExecutingTask } from '@/api/modules/log';
|
||||
import { compareVersion } from '@/utils/version';
|
||||
import bus from '@/global/bus';
|
||||
import PrimaryMenu from '@/assets/images/menu-bg.svg?component';
|
||||
|
||||
const route = useRoute();
|
||||
const menuStore = MenuStore();
|
||||
@ -305,7 +309,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: url(@/assets/images/menu-bg.png) var(--el-menu-bg-color) no-repeat top;
|
||||
background: var(--panel-menu-bg-color) no-repeat top;
|
||||
|
||||
.el-scrollbar {
|
||||
flex: 1;
|
||||
|
@ -146,7 +146,7 @@ onMounted(() => {
|
||||
height: 100vh;
|
||||
transition: margin-left 0.3s;
|
||||
margin-left: var(--panel-menu-width);
|
||||
background-color: #f4f4f4;
|
||||
background-color: var(--panel-main-bg-color-9);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.app-main {
|
||||
|
@ -4,13 +4,13 @@ export interface ThemeConfigProp {
|
||||
panelName: string;
|
||||
primary: string;
|
||||
theme: string; // dark | bright | auto
|
||||
isGold: boolean;
|
||||
footer: boolean;
|
||||
|
||||
title: string;
|
||||
logo: string;
|
||||
logoWithText: string;
|
||||
favicon: string;
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
export interface GlobalState {
|
||||
|
@ -14,11 +14,10 @@ const GlobalStore = defineStore({
|
||||
language: '',
|
||||
themeConfig: {
|
||||
panelName: '',
|
||||
primary: '#005EEB',
|
||||
primary: '#005eeb',
|
||||
theme: 'auto',
|
||||
isGold: false,
|
||||
footer: true,
|
||||
|
||||
themeColor: '',
|
||||
title: '',
|
||||
logo: '',
|
||||
logoWithText: '',
|
||||
@ -48,10 +47,8 @@ const GlobalStore = defineStore({
|
||||
getters: {
|
||||
isDarkTheme: (state) =>
|
||||
state.themeConfig.theme === 'dark' ||
|
||||
state.themeConfig.isGold ||
|
||||
(state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches),
|
||||
isDarkGoldTheme: (state) => state.themeConfig.isGold && state.isMasterProductPro,
|
||||
isMaster: (state) => state.currentNode === 'local',
|
||||
isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro,
|
||||
},
|
||||
actions: {
|
||||
setOpenMenuTabs(openMenuTabs: boolean) {
|
||||
|
@ -124,7 +124,7 @@ html {
|
||||
.input-help {
|
||||
font-size: 12px;
|
||||
word-break: keep-all;
|
||||
color: #adb0bc;
|
||||
color: #ADB0BC;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
@ -220,7 +220,6 @@ html {
|
||||
background: var(--el-button-bg-color, var(--el-fill-color-blank));
|
||||
border: 0;
|
||||
font-weight: 350;
|
||||
border-left: 0;
|
||||
color: var(--el-button-text-color, var(--el-text-color-regular));
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
@ -348,7 +347,7 @@ html {
|
||||
|
||||
.el-input-group__append {
|
||||
border-left: 0;
|
||||
background-color: #ffffff !important;
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
box-shadow: 0 1px 0 0 var(--el-input-border-color) inset, 0 -1px 0 0 var(--el-input-border-color) inset,
|
||||
@ -452,3 +451,25 @@ html {
|
||||
.monaco-editor-tree-dark .el-tree-node.is-current > .el-tree-node__content {
|
||||
background-color: #111417;
|
||||
}
|
||||
|
||||
.check-label{
|
||||
background: var(--panel-main-bg-color-10) !important;
|
||||
.check-label-a {
|
||||
color: var(--panel-color-primary);
|
||||
}
|
||||
}
|
||||
.check-content {
|
||||
background: var(--panel-main-bg-color-10);
|
||||
pre {
|
||||
margin: 0;
|
||||
width: 350px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-descriptions {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -1,273 +1,350 @@
|
||||
html.dark {
|
||||
--el-box-shadow-light: 0px 0px 4px rgba(0, 0, 0, 0.1) !important;
|
||||
--dark-gold-base-color: #5a5a5a;
|
||||
--el-border-color-lighter: #1d2023;
|
||||
--el-fill-color-blank: #111417;
|
||||
--el-bg-color: rgba(0, 11, 21, 1);
|
||||
// --el-text-color-primary: #999999;
|
||||
--el-text-color-regular: #bbbfc4 !important;
|
||||
--el-fill-color-light: #111417;
|
||||
--el-border-color: #303438;
|
||||
--el-bg-color-overlay: rgba(0, 11, 21, 1);
|
||||
--el-border-color-light: #1d2023;
|
||||
// * menu
|
||||
--el-menu-bg-color: #111417 !important;
|
||||
--el-menu-item-bg-color: #111417;
|
||||
--el-menu-text-color: #ffffff;
|
||||
--el-menu-item-bg-color-active: rgb(44, 45, 46);
|
||||
--panel-color-primary: #3d8eff;
|
||||
--panel-color-primary-light-8: #3674cc;
|
||||
--panel-color-primary-light-1: #6eaaff;
|
||||
--panel-color-primary-light-2: #366fc2;
|
||||
--panel-color-primary-light-3: #3364ad;
|
||||
--panel-color-primary-light-4: #2f558f;
|
||||
--panel-color-primary-light-5: #372e46;
|
||||
--panel-color-primary-light-6: #2a4066;
|
||||
--panel-color-primary-light-7: #2d4a7a;
|
||||
--panel-color-primary-light-9: #2d4a7a;
|
||||
|
||||
// * panel-admin
|
||||
--panel-text-color: rgb(174, 166, 153);
|
||||
--panel-border: 1px solid #1d2023;
|
||||
--panel-border-color: #394c5e;
|
||||
--panel-main-bg-color: rgba(12, 12, 12, 1);
|
||||
--panel-button-active: var(--el-color-primary);
|
||||
--panel-main-bg-color-1: #e3e6f3;
|
||||
--panel-main-bg-color-2: #c0c2cf;
|
||||
--panel-main-bg-color-3: #adb0bc;
|
||||
--panel-main-bg-color-4: #9597a4;
|
||||
--panel-main-bg-color-5: #90929f;
|
||||
--panel-main-bg-color-6: #787b88;
|
||||
--panel-main-bg-color-7: #5b5e6a;
|
||||
--panel-main-bg-color-8: #434552;
|
||||
--panel-main-bg-color-9: #2e313d;
|
||||
--panel-main-bg-color-10: #242633;
|
||||
--panel-main-bg-color-11: #60626f;
|
||||
--panel-main-bg-color-12: #000000;
|
||||
|
||||
--panel-login-shadow-light: 5px 5px 15px rgb(255 255 255 / 20%);
|
||||
--panel-box-shadow-light: 0 0 10px rgb(255 255 255 / 10%);
|
||||
--panel-popup-color: #060708;
|
||||
--panel-alert-bg: #2f3030;
|
||||
--panel-path-bg: #2f3030;
|
||||
--panel-button-disabled: #5a5a5a;
|
||||
--panel-alert-error-bg-color: #fef0f0;
|
||||
--panel-alert-error-text-color: #f56c6c;
|
||||
--panel-alert-error-hover-bg-color: #e9657b;
|
||||
|
||||
.el-tag.el-tag--info {
|
||||
--el-tag-bg-color: rgb(49, 51, 51);
|
||||
--el-tag-border-color: rgb(64, 67, 67);
|
||||
--panel-alert-success-bg-color: #e1f3d8;
|
||||
--panel-alert-success-text-color: #67c23a;
|
||||
--panel-alert-success-hover-bg-color: #4dc894;
|
||||
|
||||
--panel-alert-warning-bg-color: #59472a;
|
||||
--panel-alert-warning-text-color: #edac2c;
|
||||
--panel-alert-warning-hover-bg-color: #f1c161;
|
||||
|
||||
--panel-alert-info-bg-color: var(--panel-main-bg-color-7);
|
||||
--panel-alert-info-text-color: var(--panel-text-color-white);
|
||||
--panel-alert-info-hover-bg-color: var(--panel-main-bg-color-4);
|
||||
|
||||
--el-color-success: #3fb950;
|
||||
--el-color-success-light-5: #4dc894;
|
||||
--el-color-success-light-8: #3fb950;
|
||||
--el-color-success-light-9: var(--panel-main-bg-color-9);
|
||||
|
||||
--el-color-warning: #edac2c;
|
||||
--el-color-warning-light-5: #f1c161;
|
||||
--el-color-warning-light-8: #edac2c;
|
||||
--el-color-warning-light-9: var(--panel-main-bg-color-9);
|
||||
|
||||
--el-color-danger: #e2324f;
|
||||
--el-color-danger-light-5: #e9657b;
|
||||
--el-color-danger-light-8: #e2324f;
|
||||
--el-color-danger-light-9: var(--panel-main-bg-color-9);
|
||||
|
||||
--el-color-error: #e2324f;
|
||||
--el-color-error-light-5: #e9657b;
|
||||
--el-color-error-light-8: #e2324f;
|
||||
--el-color-error-light-9: var(--panel-main-bg-color-9);
|
||||
|
||||
--el-color-info: var(--panel-main-bg-color-3);
|
||||
--el-color-info-light-5: var(--panel-main-bg-color-3);
|
||||
--el-color-info-light-8: var(--panel-main-bg-color-3);
|
||||
--el-color-info-light-9: var(--panel-main-bg-color-9);
|
||||
|
||||
--panel-pie-bg-color: #434552;
|
||||
--panel-text-color-white: #ffffff;
|
||||
|
||||
--el-color-primary: var(--panel-color-primary);
|
||||
--el-color-primary-light-1: var(--panel-color-primary-light-1);
|
||||
--el-color-primary-light-2: var(--panel-color-primary-light-2);
|
||||
--el-color-primary-light-3: var(--panel-color-primary-light-3);
|
||||
--el-color-primary-light-4: var(--panel-color-primary-light-4);
|
||||
--el-color-primary-light-5: var(--panel-color-primary-light-5);
|
||||
--el-color-primary-light-6: var(--panel-color-primary-light-6);
|
||||
--el-color-primary-light-7: var(--panel-color-primary-light-7);
|
||||
--el-color-primary-light-8: var(--panel-color-primary-light-8);
|
||||
--el-color-primary-light-9: var(--panel-color-primary-light-9);
|
||||
--el-color-primary-dark-2: var(--panel-color-primary);
|
||||
--el-scrollbar-bg-color: var(--panel-main-bg-color-8);
|
||||
--el-border-color-darker: var(--panel-main-bg-color-6);
|
||||
|
||||
--panel-border: 1px solid var(--panel-main-bg-color-8);
|
||||
--panel-border-color: var(--panel-main-bg-color-8);
|
||||
--panel-button-active: var(--panel-main-bg-color-10);
|
||||
--panel-button-text-color: var(--panel-main-bg-color-10);
|
||||
--panel-button-bg-color: var(--panel-color-primary);
|
||||
--panel-footer-bg: var(--panel-main-bg-color-9);
|
||||
--panel-footer-border: var(--panel-main-bg-color-7);
|
||||
--panel-text-color: var(--panel-main-bg-color-1);
|
||||
--panel-menu-bg-color: var(--panel-main-bg-color-10);
|
||||
--panel-terminal-tag-bg-color: var(--panel-main-bg-color-10);
|
||||
--panel-terminal-tag-active-bg-color: var(--panel-main-bg-color-10);
|
||||
--panel-terminal-bg-color: var(--panel-main-bg-color-10);
|
||||
--panel-terminal-tag-active-text-color: var(--panel-color-primary);
|
||||
--panel-terminal-tag-hover-text-color: var(--panel-color-primary);
|
||||
--panel-logs-bg-color: var(--panel-main-bg-color-9);
|
||||
--panel-alert-bg-color: var(--panel-main-bg-color-10);
|
||||
|
||||
--el-menu-item-bg-color: var(--panel-main-bg-color-10);
|
||||
--el-menu-item-bg-color-active: var(--panel-main-bg-color-8);
|
||||
--el-menu-hover-bg-color: var(--panel-main-bg-color-8);
|
||||
--el-menu-text-color: var(--panel-main-bg-color-2);
|
||||
--el-fill-color-blank: var(--panel-main-bg-color-10);
|
||||
--el-fill-color-light: var(--panel-main-bg-color-10);
|
||||
--el-border-color: var(--panel-main-bg-color-8);
|
||||
--el-border-color-light: var(--panel-main-bg-color-8);
|
||||
--el-border-color-lighter: var(--panel-main-bg-color-8);
|
||||
|
||||
--el-text-color-primary: var(--panel-main-bg-color-2);
|
||||
--el-text-color-regular: var(--panel-main-bg-color-2);
|
||||
|
||||
--el-box-shadow: 0px 12px 32px 4px rgba(36, 38, 51, 0.36), 0px 8px 20px rgba(36, 38, 51, 0.72);
|
||||
--el-box-shadow-light: 0px 0px 12px rgba(36, 38, 51, 0.72);
|
||||
--el-box-shadow-lighter: 0px 0px 6px rgba(36, 38, 51, 0.72);
|
||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(36, 38, 51, 0.72), 0px 12px 32px #242633, 0px 8px 16px -8px #242633;
|
||||
--el-bg-color: var(--panel-main-bg-color-9);
|
||||
--el-bg-color-overlay: var(--panel-main-bg-color-9);
|
||||
|
||||
--el-text-color-placeholder: var(--panel-main-bg-color-4);
|
||||
|
||||
.el-radio-button {
|
||||
--el-radio-button-checked-text-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
.el-tag.el-tag--light {
|
||||
--el-tag-bg-color: #111417;
|
||||
--el-tag-border-color: var(--el-color-primary);
|
||||
}
|
||||
.el-tag.el-tag--success {
|
||||
--el-tag-border-color: var(--el-color-success);
|
||||
}
|
||||
.el-tag.el-tag--danger {
|
||||
--el-tag-border-color: var(--el-color-danger);
|
||||
}
|
||||
.el-card {
|
||||
--el-card-bg-color: rgb(35, 35, 35);
|
||||
color: #ffffff;
|
||||
border: 1px solid var(--el-card-border-color) !important;
|
||||
.el-descriptions__content:not(.is-bordered-label) {
|
||||
color: var(--panel-main-bg-color-3);
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--el-table-bg-color: rgba(0, 11, 21, 1);
|
||||
--el-table-tr-bg-color: rgba(0, 11, 21, 1);
|
||||
--el-table-header-bg-color: rgba(0, 11, 21, 1);
|
||||
--el-table-border: var(--panel-border);
|
||||
--el-table-border-color: rgb(64, 67, 67);
|
||||
}
|
||||
.el-message-box {
|
||||
--el-messagebox-title-color: var(--el-menu-text-color);
|
||||
border: 1px solid var(--panel-border-color);
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
background: var(--panel-main-bg-color-8) !important;
|
||||
}
|
||||
|
||||
.el-alert--info {
|
||||
--el-alert-bg-color: rgb(56, 59, 59);
|
||||
.el-menu .el-menu-item {
|
||||
box-shadow: 0 0 4px rgba(36, 38, 51, 0.72);
|
||||
}
|
||||
|
||||
.el-menu .el-sub-menu__title {
|
||||
box-shadow: 0 0 4px rgba(36, 38, 51, 0.72);
|
||||
}
|
||||
|
||||
.el-overlay {
|
||||
background-color: rgb(46 49 61 / 80%);
|
||||
}
|
||||
|
||||
.el-tag.el-tag--primary {
|
||||
--el-tag-bg-color: var(--panel-main-bg-color-9);
|
||||
--el-tag-border-color: var(--panel-main-bg-color-11);
|
||||
--el-tag-hover-color: var(--panel-color-primary);
|
||||
}
|
||||
|
||||
.el-tabs--card > .el-tabs__header .el-tabs__nav {
|
||||
border: 1px solid var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
|
||||
border-bottom-color: var(--panel-color-primary);
|
||||
--el-text-color-regular: var(--panel-color-primary);
|
||||
}
|
||||
.main-container {
|
||||
.el-loading-mask {
|
||||
background-color: #24263375;
|
||||
}
|
||||
}
|
||||
|
||||
.el-loading-mask {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.el-input {
|
||||
--el-input-bg-color: rgb(47 48 48);
|
||||
--el-input-border-color: #303438;
|
||||
--el-input-border-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
--el-pagination-button-color: #999999;
|
||||
}
|
||||
|
||||
.el-popover {
|
||||
--el-popover-title-text-color: #999999;
|
||||
border: 1px solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
.md-editor-dark {
|
||||
--md-bk-color: #111417;
|
||||
}
|
||||
|
||||
// * 以下为自定义暗黑模式内容
|
||||
// login
|
||||
.login-container {
|
||||
.login-form {
|
||||
input:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px #f1f4f9 inset;
|
||||
-webkit-text-fill-color: #333333;
|
||||
-webkit-transition: background-color 1000s ease-out 0.5s;
|
||||
box-shadow: 0 0 0 1000px var(--el-box-shadow) inset;
|
||||
background-color: var(--panel-main-bg-color-1);
|
||||
transition: background-color 1000s ease-out 0.5s;
|
||||
}
|
||||
.el-input__wrapper {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 0 0 1px #dcdfe6 inset;
|
||||
|
||||
.el-input.is-disabled .el-input__wrapper {
|
||||
--el-disabled-bg-color: var(--panel-main-bg-color-9);
|
||||
--el-disabled-border-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
.el-input__inner {
|
||||
color: #606266;
|
||||
|
||||
.el-input > .el-input-group__append:hover {
|
||||
background-color: var(--panel-main-bg-color-9) !important;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
color: var(--panel-main-bg-color-3);
|
||||
}
|
||||
|
||||
.el-card {
|
||||
--el-card-bg-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
|
||||
.el-button:hover {
|
||||
--el-button-hover-border-color: var(--panel-main-bg-color-11);
|
||||
--el-button-hover-bg-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
--el-button-text-color: var(--panel-main-bg-color-10);
|
||||
--el-button-hover-link-text-color: var(--panel-color-primary-light-1);
|
||||
&.tag-button,
|
||||
&.brief-button {
|
||||
--el-button-text-color: var(--panel-main-bg-color-10);
|
||||
--el-button-hover-text-color: var(--el-color-white);
|
||||
--el-button-hover-border-color: var(--el-color-primary);
|
||||
--el-button-hover-bg-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.app-button {
|
||||
--el-button-text-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.h-app-button {
|
||||
--el-button-text-color: var(--panel-main-bg-color-10);
|
||||
--el-button-hover-text-color: var(--el-color-white);
|
||||
--el-button-hover-border-color: var(--el-color-primary);
|
||||
--el-button-hover-bg-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// scroll-bar
|
||||
::-webkit-scrollbar {
|
||||
background-color: var(--el-scrollbar-bg-color) !important;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--el-border-color-darker);
|
||||
}
|
||||
// sidebar
|
||||
.sidebar-container-popper {
|
||||
border: 1px solid #66686c;
|
||||
.el-menu--popup-container {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.sidebar-container {
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
.el-menu {
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
background: rgba(37, 39, 44, 1);
|
||||
}
|
||||
&.is-active {
|
||||
background: var(--el-color-primary);
|
||||
color: #ffffff;
|
||||
&:hover {
|
||||
.el-icon {
|
||||
color: #ffffff !important;
|
||||
.el-button--primary.is-plain,
|
||||
.el-button--primary.is-text,
|
||||
.el-button--primary.is-link {
|
||||
--el-button-text-color: var(--panel-main-bg-color-2);
|
||||
--el-button-bg-color: var(--panel-main-bg-color-9);
|
||||
--el-button-border-color: var(--panel-main-bg-color-8);
|
||||
--el-button-hover-bg-color: var(--panel-main-bg-color-9);
|
||||
--el-button-hover-border-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
span {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-sub-menu {
|
||||
.el-sub-menu__title {
|
||||
&:hover {
|
||||
background: rgba(37, 39, 44, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-collapse {
|
||||
color: var(--el-menu-text-color);
|
||||
border: var(--panel-border);
|
||||
.el-button--primary.is-text,
|
||||
.el-button--primary.is-link {
|
||||
--el-button-text-color: var(--panel-color-primary);
|
||||
}
|
||||
|
||||
// layout
|
||||
.app-wrapper {
|
||||
.main-container {
|
||||
background-color: var(--panel-main-bg-color) !important;
|
||||
}
|
||||
.app-footer {
|
||||
background-color: var(--panel-main-bg-color) !important;
|
||||
border-top: var(--panel-border);
|
||||
}
|
||||
.mobile-header {
|
||||
background-color: var(--panel-main-bg-color) !important;
|
||||
border-bottom: var(--panel-border);
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.system-label {
|
||||
color: var(--el-menu-text-color);
|
||||
.el-button--primary:hover {
|
||||
--el-button-hover-text-color: var(--panel-main-bg-color-7);
|
||||
--el-button-border-color: var(--el-color-primary);
|
||||
--el-button-hover-bg-color: var(--panel-color-primary-light-2);
|
||||
--el-button-hover-border-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.router_card_button {
|
||||
.el-radio-button__inner {
|
||||
background: none !important;
|
||||
.el-button--primary.is-plain:hover {
|
||||
--el-button-hover-text-color: var(--panel-main-bg-color-10);
|
||||
--el-button-border-color: var(--el-color-primary);
|
||||
--el-button-hover-bg-color: var(--el-color-primary);
|
||||
--el-button-hover-border-color: var(--el-color-primary);
|
||||
}
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: #ffffff;
|
||||
background-color: var(--panel-button-active) !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
|
||||
.el-button--primary:active {
|
||||
--el-button-hover-text-color: var(--panel-main-bg-color-7);
|
||||
--el-button-active-bg-color: var(--el-color-primary-light-3);
|
||||
--el-button-active-border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
.el-button--primary.is-plain:active {
|
||||
color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
// content-box
|
||||
.content-box {
|
||||
.text {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
|
||||
.el-button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.el-button.is-disabled {
|
||||
color: var(--panel-main-bg-color-7);
|
||||
border-color: var(--panel-main-bg-color-8);
|
||||
background: var(--panel-main-bg-color-9);
|
||||
}
|
||||
|
||||
.el-button.is-disabled:hover {
|
||||
border-color: var(--panel-main-bg-color-8);
|
||||
background: var(--panel-main-bg-color-9);
|
||||
}
|
||||
|
||||
.el-button--primary.is-link.is-disabled {
|
||||
color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: var(--panel-main-bg-color-7);
|
||||
}
|
||||
|
||||
.el-drawer .el-drawer__header span {
|
||||
color: var(--el-menu-text-color);
|
||||
}
|
||||
.el-drawer {
|
||||
border-left: 0.5px solid var(--panel-border-color);
|
||||
color: var(--panel-text-color);
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: var(--el-disabled-bg-color);
|
||||
}
|
||||
.el-input.is-disabled .el-input__wrapper {
|
||||
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
|
||||
}
|
||||
.el-radio-button__inner {
|
||||
background: #1d2023;
|
||||
}
|
||||
.el-button--primary.is-plain.is-disabled {
|
||||
background: #1d2023;
|
||||
border-color: #303438;
|
||||
}
|
||||
.el-button--primary.is-plain {
|
||||
background: #1d2023;
|
||||
border-color: #303438;
|
||||
}
|
||||
.el-button.is-link.is-disabled {
|
||||
color: var(--panel-button-disabled) !important;
|
||||
}
|
||||
.el-button.is-disabled {
|
||||
border-color: #303438;
|
||||
color: var(--panel-button-disabled);
|
||||
}
|
||||
.el-popper.is-dark {
|
||||
color: rgb(171 173 173);
|
||||
}
|
||||
.path {
|
||||
border: var(--panel-border);
|
||||
.split {
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
input:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px #323232 inset;
|
||||
-webkit-text-fill-color: #cfd3dc;
|
||||
transition: background-color 1000s ease-out 0.5s;
|
||||
}
|
||||
.el-avatar {
|
||||
--el-avatar-bg-color: #111417 !important;
|
||||
box-shadow: 0px 0px 4px rgba(0, 94, 235, 0.1);
|
||||
border: 0.5px solid #1f2022;
|
||||
}
|
||||
.el-page-header__content {
|
||||
color: rgb(174, 166, 153);
|
||||
}
|
||||
.el-dialog {
|
||||
background-color: var(--panel-main-bg-color-9);
|
||||
border: 1px solid var(--panel-border-color);
|
||||
|
||||
.el-dialog__header {
|
||||
border-bottom: var(--panel-border);
|
||||
color: #999999;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.el-dialog__title {
|
||||
color: var(--el-menu-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-tabs__item {
|
||||
color: #999999;
|
||||
|
||||
.el-alert--error {
|
||||
--el-alert-bg-color: var(--panel-alert-error-bg-color);
|
||||
--el-color-error: var(--panel-alert-error-text-color);
|
||||
}
|
||||
.el-tabs__item.is-active {
|
||||
color: var(--el-color-primary) !important;
|
||||
|
||||
.el-alert--success {
|
||||
--el-alert-bg-color: var(--panel-alert-success-bg-color);
|
||||
--el-color-success: var(--panel-alert-success-text-color);
|
||||
}
|
||||
|
||||
.el-alert--warning {
|
||||
--el-alert-bg-color: var(--panel-alert-warning-bg-color);
|
||||
--el-color-warning: var(--panel-alert-warning-text-color);
|
||||
}
|
||||
|
||||
.el-alert--info {
|
||||
--el-alert-bg-color: var(--panel-alert-info-bg-color);
|
||||
--el-color-info: var(--panel-alert-info-text-color);
|
||||
}
|
||||
|
||||
.md-editor-dark {
|
||||
--md-bk-color: var(--panel-main-bg-color-9);
|
||||
}
|
||||
|
||||
.md-editor-dark .md-editor-preview {
|
||||
--md-theme-color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.md-editor-dark .default-theme a {
|
||||
--md-theme-link-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.md-editor-dark .default-theme pre code {
|
||||
background-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
.md-editor-dark .default-theme pre:before {
|
||||
background-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
|
||||
.el-descriptions__title {
|
||||
color: #999999;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.el-descriptions__content.el-descriptions__cell.is-bordered-content {
|
||||
color: #999999;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.el-descriptions--large .el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell {
|
||||
padding: 12px 15px;
|
||||
@ -281,94 +358,121 @@ html.dark {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.terminal-tabs {
|
||||
.el-avatar {
|
||||
--el-avatar-bg-color: var(--panel-text-color-white) !important;
|
||||
box-shadow: 0 0 4px rgba(0, 94, 235, 0.1);
|
||||
border: 0.5px solid var(--panel-main-bg-color-7);
|
||||
}
|
||||
.el-drawer {
|
||||
.cm-editor {
|
||||
background-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
.cm-gutters {
|
||||
background-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
.log-container {
|
||||
background-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
background-color: var(--panel-main-bg-color-9);
|
||||
}
|
||||
.cm-gutters {
|
||||
background-color: var(--panel-main-bg-color-9);
|
||||
}
|
||||
|
||||
// scroll-bar
|
||||
::-webkit-scrollbar {
|
||||
background-color: var(--el-scrollbar-bg-color) !important;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--el-border-color-darker);
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: var(--el-scrollbar-bg-color);
|
||||
}
|
||||
|
||||
.app-warn {
|
||||
span {
|
||||
&:nth-child(2) {
|
||||
color: var(--panel-color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--panel-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--el-table-bg-color: var(--el-bg-color);
|
||||
--el-table-tr-bg-color: var(--el-bg-color);
|
||||
--el-table-header-bg-color: var(--el-bg-color);
|
||||
--el-table-border: 1px solid var(--panel-main-bg-color-8);
|
||||
--el-table-border-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
--el-messagebox-title-color: var(--el-menu-text-color);
|
||||
border: 1px solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
.el-popover {
|
||||
--el-popover-title-text-color: var(--panel-main-bg-color-2);
|
||||
border: 1px solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
.main-container {
|
||||
background-color: var(--panel-main-bg-color-9) !important;
|
||||
}
|
||||
.app-footer {
|
||||
background-color: var(--panel-main-bg-color-9) !important;
|
||||
border-top: var(--panel-border);
|
||||
}
|
||||
.mobile-header {
|
||||
background-color: var(--panel-main-bg-color-9) !important;
|
||||
border-bottom: var(--panel-border);
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.router_card_button {
|
||||
.el-radio-button__inner {
|
||||
background: none !important;
|
||||
.el-tabs__header {
|
||||
background: #000000;
|
||||
}
|
||||
}
|
||||
.el-pager {
|
||||
li {
|
||||
color: #999999;
|
||||
&.is-active {
|
||||
color: var(--el-pagination-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-loading-mask {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.h-app-card {
|
||||
.h-app-content {
|
||||
.h-app-title {
|
||||
color: #f2f8ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infinite-list .infinite-list-item {
|
||||
background: #212426;
|
||||
&:hover {
|
||||
background: #212426;
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: var(--panel-main-bg-color-10);
|
||||
background-color: var(--panel-color-primary) !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-alert--warning.is-light {
|
||||
background-color: rgb(56, 59, 59);
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
.el-dropdown-menu__item.is-disabled {
|
||||
color: var(--panel-button-disabled);
|
||||
}
|
||||
.el-date-editor .el-range-separator {
|
||||
color: var(--panel-button-disabled);
|
||||
}
|
||||
|
||||
.el-input-group__append {
|
||||
border-left: 0;
|
||||
background-color: #212426 !important;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
box-shadow: 0 1px 0 0 var(--el-input-border-color) inset, 0 -1px 0 0 var(--el-input-border-color) inset,
|
||||
-1px 0 0 0 var(--el-input-border-color) inset;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
.el-date-table td.in-range .el-date-table-cell {
|
||||
background-color: var(--panel-main-bg-color-8);
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
color: #ffffff;
|
||||
background-color: var(--panel-main-bg-color-10) !important;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #4f4f4f;
|
||||
.el-checkbox__input.is-checked .el-checkbox__inner::after {
|
||||
border-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
|
||||
.level-up-pro {
|
||||
--dark-gold-base-color: #eaba63;
|
||||
--dark-gold-text-color: #1f2329;
|
||||
--el-color-primary: var(--dark-gold-base-color);
|
||||
|
||||
.title {
|
||||
color: var(--dark-gold-base-color);
|
||||
}
|
||||
.el-button--primary {
|
||||
&.is-plain {
|
||||
background: var(--dark-gold-base-color);
|
||||
--el-button-text-color: var(--dark-gold-text-color);
|
||||
|
||||
&.is-disabled {
|
||||
background: #1d2023;
|
||||
border-color: #303438;
|
||||
}
|
||||
.el-checkbox__input.is-indeterminate .el-checkbox__inner::before {
|
||||
background-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
|
||||
&.is-text {
|
||||
--el-button-text-color: var(--dark-gold-base-color);
|
||||
}
|
||||
.custom-input-textarea {
|
||||
background-color: var(--panel-main-bg-color-10) !important;
|
||||
color: var(--el-color-info) !important;
|
||||
}
|
||||
.custom-input-textarea:hover {
|
||||
background-color: var(--panel-main-bg-color-9) !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
@ -1,34 +1,63 @@
|
||||
:root {
|
||||
--el-color-primary: #005eeb;
|
||||
--el-color-primary-dark-2: #0054d3;
|
||||
--panel-gradient-end-color: var(--el-color-primary-light-7);
|
||||
|
||||
--el-color-primary-light-1: #196eed;
|
||||
--el-color-primary-light-2: #337eef;
|
||||
--el-color-primary-light-3: #4c8ef1;
|
||||
--el-color-primary-light-4: #669ef3;
|
||||
--el-color-primary-light-5: #7faef5;
|
||||
--el-color-primary-light-6: #99bef7;
|
||||
--el-color-primary-light-7: #b2cef9;
|
||||
--el-color-primary-light-8: #ccdefb;
|
||||
--el-color-primary-light-9: #e5eefd;
|
||||
--el-font-weight-primary: 400;
|
||||
|
||||
--panel-color-primary: #005eeb;
|
||||
--panel-color-primary-light-8: #196eed;
|
||||
--panel-color-primary-light-1: #196eed;
|
||||
--panel-color-primary-light-2: #337eef;
|
||||
--panel-color-primary-light-3: #4c8ef1;
|
||||
--panel-color-primary-light-4: #669ef3;
|
||||
--panel-color-primary-light-5: #7faef5;
|
||||
--panel-color-primary-light-6: #99bef7;
|
||||
--panel-color-primary-light-7: #b2cef9;
|
||||
--panel-color-primary-light-9: #e5eefd;
|
||||
|
||||
--el-color-primary: var(--panel-color-primary);
|
||||
--el-color-primary-light-3: var(--panel-color-primary-light-1);
|
||||
--el-color-primary-light-5: var(--panel-color-primary-light-5);
|
||||
--el-color-primary-light-7: var(--panel-color-primary-light-7);
|
||||
--el-color-primary-light-8: var(--panel-color-primary-light-8);
|
||||
--el-color-primary-light-9: var(--panel-color-primary-light-9);
|
||||
|
||||
--el-text-color-regular: #646a73;
|
||||
}
|
||||
|
||||
html {
|
||||
--el-box-shadow-light: 0px 0px 4px rgba(0, 94, 235, 0.1) !important;
|
||||
|
||||
--el-text-color-regular: #646a73 !important;
|
||||
// * menu
|
||||
--el-menu-bg-color: rgba(0, 94, 235, 0.1) !important;
|
||||
--el-menu-item-bg-color: rgba(255, 255, 255, 0.3);
|
||||
--el-menu-item-bg-color-active: #ffffff;
|
||||
--panel-main-bg-color-9: #f4f4f4;
|
||||
--panel-menu-bg-color: rgba(0, 94, 235, 0.1);
|
||||
--panel-menu-width: 180px;
|
||||
--panel-menu-hide-width: 75px;
|
||||
--panel-text-color: #1f2329;
|
||||
--panel-border: 1px solid #f2f2f2;
|
||||
--panel-button-active: #ffffff;
|
||||
--panel-button-text-color: var(--panel-color-primary);
|
||||
--panel-button-bg-color: #ffffff;
|
||||
--panel-footer-border: #e4e7ed;
|
||||
--panel-terminal-tag-bg-color: #efefef;
|
||||
--panel-terminal-tag-active-bg-color: #575758;
|
||||
--panel-terminal-tag-active-text-color: #ebeef5;
|
||||
--panel-terminal-tag-hover-text-color: #575758;
|
||||
--panel-terminal-bg-color: #1e1e1e;
|
||||
--panel-logs-bg-color: #1e1e1e;
|
||||
--panel-alert-bg-color: rgba(0, 94, 235, 0.03);
|
||||
|
||||
--panel-alert-bg: #e2e4ec;
|
||||
--panel-path-bg: #ffffff;
|
||||
--panel-footer-bg: #ffffff;
|
||||
--panel-pie-bg-color: #ffffff;
|
||||
|
||||
--el-fill-color-light: #ffffff;
|
||||
--el-disabled-bg-color: var(--panel-main-bg-color-9) !important;
|
||||
}
|
||||
|
||||
.el-notification {
|
||||
@ -214,6 +243,10 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.el-input.is-disabled .el-input__wrapper {
|
||||
--el-disabled-bg-color: var(--panel-main-bg-color-9);
|
||||
}
|
||||
|
||||
.el-radio-button__inner {
|
||||
[class*='el-icon'] + span {
|
||||
margin-left: 6px;
|
||||
@ -226,3 +259,14 @@ html {
|
||||
.logo {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.custom-input-textarea {
|
||||
background-color: #f5f7fa !important;
|
||||
color: var(--el-color-info) !important;
|
||||
}
|
||||
|
||||
.custom-input-textarea:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
border-color: var(--el-button-border-color) !important;
|
||||
}
|
@ -10,3 +10,21 @@ body,
|
||||
:-webkit-any(article, aside, nav, section) h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.el-switch--small .el-switch__core {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.el-switch--small .el-switch__core::after {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.el-switch--small.is-checked .el-switch__core::after {
|
||||
margin-left: -13px;
|
||||
}
|
||||
|
||||
.el-alert__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
8
frontend/src/utils/theme.ts
Normal file
8
frontend/src/utils/theme.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export function setPrimaryColor(color: string) {
|
||||
let setPrimaryColor: (arg0: string) => any;
|
||||
const xpackModules = import.meta.glob('../xpack/utils/theme/tool.ts', { eager: true });
|
||||
if (xpackModules['../xpack/utils/theme/tool.ts']) {
|
||||
setPrimaryColor = xpackModules['../xpack/utils/theme/tool.ts']['setPrimaryColor'] || {};
|
||||
return setPrimaryColor(color);
|
||||
}
|
||||
}
|
@ -237,6 +237,8 @@ let icons = new Map([
|
||||
['.zip', 'p-file-zip'],
|
||||
['.gz', 'p-file-zip'],
|
||||
['.tar.bz2', 'p-file-zip'],
|
||||
['.bz2', 'p-file-zip'],
|
||||
['.xz', 'p-file-zip'],
|
||||
['.tar', 'p-file-zip'],
|
||||
['.tar.gz', 'p-file-zip'],
|
||||
['.war', 'p-file-zip'],
|
||||
@ -635,7 +637,7 @@ export function emptyLineFilter(str: string, spilt: string) {
|
||||
// 文件类型映射
|
||||
let fileTypes = {
|
||||
image: ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.ico', '.svg', '.webp'],
|
||||
compress: ['.zip', '.rar', '.gz', '.war', '.tgz', '.7z', '.tar.gz', '.tar'],
|
||||
compress: ['.zip', '.rar', '.gz', '.war', '.tgz', '.7z', '.tar.gz', '.tar', '.bz2', '.xz', '.tar.bz2', '.tar.xz'],
|
||||
video: ['.mp4', '.webm', '.mov', '.wmv', '.mkv', '.avi', '.wma', '.flv'],
|
||||
audio: ['.mp3', '.wav', '.wma', '.ape', '.acc', '.ogg', '.flac'],
|
||||
pdf: ['.pdf'],
|
||||
@ -657,3 +659,29 @@ export const getFileType = (extension: string) => {
|
||||
export const newUUID = () => {
|
||||
return uuidv4();
|
||||
};
|
||||
|
||||
export const escapeProxyURL = (url: string): string => {
|
||||
const encodeMap: { [key: string]: string } = {
|
||||
':': '%%3A',
|
||||
'/': '%%2F',
|
||||
'?': '%%3F',
|
||||
'#': '%%23',
|
||||
'[': '%%5B',
|
||||
']': '%%5D',
|
||||
'@': '%%40',
|
||||
'!': '%%21',
|
||||
$: '%%24',
|
||||
'&': '%%26',
|
||||
"'": '%%27',
|
||||
'(': '%%28',
|
||||
')': '%%29',
|
||||
'*': '%%2A',
|
||||
'+': '%%2B',
|
||||
',': '%%2C',
|
||||
';': '%%3B',
|
||||
'=': '%%3D',
|
||||
'%': '%%25',
|
||||
};
|
||||
|
||||
return url.replace(/[\/:?#[\]@!$&'()*+,;=%~]/g, (match) => encodeMap[match] || match);
|
||||
};
|
||||
|
@ -9,7 +9,6 @@ export function resetXSetting() {
|
||||
globalStore.themeConfig.logo = '';
|
||||
globalStore.themeConfig.logoWithText = '';
|
||||
globalStore.themeConfig.favicon = '';
|
||||
globalStore.themeConfig.isGold = false;
|
||||
}
|
||||
|
||||
export function initFavicon() {
|
||||
@ -18,13 +17,26 @@ export function initFavicon() {
|
||||
const link = (document.querySelector("link[rel*='icon']") || document.createElement('link')) as HTMLLinkElement;
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'shortcut icon';
|
||||
if (globalStore.isDarkGoldTheme) {
|
||||
let goldLink = new URL(`../assets/images/favicon-gold.png`, import.meta.url).href;
|
||||
link.href = favicon ? '/api/v2/images/favicon' : goldLink;
|
||||
let goldLink = new URL(`../assets/images/favicon.svg`, import.meta.url).href;
|
||||
if (globalStore.isProductPro) {
|
||||
const themeColor = globalStore.themeConfig.primary;
|
||||
const svg = `
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="${themeColor}" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.1451 18.8875L5.66228 15.7224V8.40336L3.5376 7.1759V16.9488L9.02038 20.114L11.1451 18.8875Z" />
|
||||
<path d="M18.3397 15.7224L12.0005 19.3819L9.87683 20.6083L12.0005 21.8348L20.4644 16.9488L18.3397 15.7224Z" />
|
||||
<path d="M12.0015 4.74388L14.1252 3.5174L12.0005 2.28995L3.5376 7.17591L5.66228 8.40337L12.0005 4.74388H12.0015Z" />
|
||||
<path d="M14.9816 4.01077L12.8569 5.23723L18.3397 8.40336V15.7224L20.4634 16.9488V7.1759L14.9816 4.01077Z" />
|
||||
<path d="M11.9995 1.02569L21.5576 6.54428V17.5795L11.9995 23.0971L2.44343 17.5795V6.54428L11.9995 1.02569ZM11.9995 0.72728L2.18182 6.39707V17.7366L11.9995 23.4064L21.8182 17.7366V6.39707L11.9995 0.72728Z" />
|
||||
<path d="M12.3079 6.78001L12.9564 7.16695V17.105L12.3079 17.48V6.78001Z" />
|
||||
<path d="M12.3078 6.78001L9.10889 8.6222V9.86954H10.2359V16.2854L12.3059 17.481L12.3078 6.78001Z" />
|
||||
</svg>
|
||||
`;
|
||||
goldLink = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
link.href = favicon ? `/api/v2/images/favicon?t=${Date.now()}` : goldLink;
|
||||
} else {
|
||||
link.href = favicon ? '/api/v2/images/favicon' : '/public/favicon.png';
|
||||
link.href = favicon ? `/api/v2/images/favicon?t=${Date.now()}` : '/public/favicon.png';
|
||||
}
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
export async function getXpackSetting() {
|
||||
@ -102,7 +114,8 @@ export async function getXpackSettingForTheme() {
|
||||
globalStore.themeConfig.logo = res2.data?.logo;
|
||||
globalStore.themeConfig.logoWithText = res2.data?.logoWithText;
|
||||
globalStore.themeConfig.favicon = res2.data?.favicon;
|
||||
globalStore.themeConfig.isGold = res2.data?.theme === 'dark-gold';
|
||||
globalStore.themeConfig.themeColor = res2.data?.themeColor;
|
||||
globalStore.themeConfig.theme = res2.data?.theme || 'auto';
|
||||
} else {
|
||||
resetXSetting();
|
||||
}
|
||||
|
@ -103,12 +103,18 @@
|
||||
</div>
|
||||
<div class="app-content">
|
||||
<div class="content-top">
|
||||
<div>
|
||||
<span class="app-name">{{ app.name }}</span>
|
||||
<el-text type="success" class="!ml-2" v-if="app.installed">
|
||||
<el-space wrap :size="1">
|
||||
<span class="app-title">{{ app.name }}</span>
|
||||
<el-tag
|
||||
type="success"
|
||||
v-if="app.installed"
|
||||
round
|
||||
size="small"
|
||||
class="!ml-2"
|
||||
>
|
||||
{{ $t('app.allReadyInstalled') }}
|
||||
</el-text>
|
||||
</div>
|
||||
</el-tag>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="content-middle">
|
||||
<span class="app-description">
|
||||
|
@ -160,7 +160,7 @@ defineExpose({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style scoped lang="scss">
|
||||
.brief {
|
||||
.name {
|
||||
span {
|
||||
@ -181,6 +181,7 @@ defineExpose({
|
||||
.icon {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.version {
|
||||
@ -191,4 +192,7 @@ defineExpose({
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
:deep(.md-editor-dark) {
|
||||
background-color: var(--panel-main-bg-color-9);
|
||||
}
|
||||
</style>
|
||||
|
@ -9,11 +9,18 @@
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<br />
|
||||
<el-descriptions border :column="1">
|
||||
<el-descriptions-item v-for="(item, key) in map" :key="key">
|
||||
<el-descriptions border :column="1" class="mt-5">
|
||||
<el-descriptions-item
|
||||
v-for="(item, key) in map"
|
||||
:key="key"
|
||||
label-class-name="check-label"
|
||||
class-name="check-content"
|
||||
min-width="60px"
|
||||
>
|
||||
<template #label>
|
||||
<a href="javascript:void(0);" @click="toPage(item[0])">{{ $t('app.' + item[0]) }}</a>
|
||||
<a href="javascript:void(0);" class="check-label-a" @click="toPage(item[0])">
|
||||
{{ $t('app.' + item[0]) }}
|
||||
</a>
|
||||
</template>
|
||||
<span class="resources">
|
||||
{{ map.get(item[0]).toString() }}
|
||||
|
@ -285,7 +285,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('container.related')" min-width="200" prop="appName">
|
||||
<el-table-column :label="$t('container.related')" min-width="210" prop="appName">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tooltip
|
||||
|
@ -114,3 +114,8 @@ defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.log-container) {
|
||||
background-color: var(--panel-main-bg-color-10);
|
||||
}
|
||||
</style>
|
||||
|
@ -378,7 +378,7 @@
|
||||
<el-form-item v-if="dialogData.rowData!.type === 'directory' && dialogData.rowData!.isDir" prop="sourceDir">
|
||||
<el-input v-model="dialogData.rowData!.sourceDir">
|
||||
<template #prepend>
|
||||
<FileList @choose="loadDir" :dir="true"></FileList>
|
||||
<FileList @choose="loadDir" :dir="true" :path="dialogData.rowData!.sourceDir" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@ -458,6 +458,31 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="hasAlert" v-if="alertTypes.includes(dialogData.rowData!.type)">
|
||||
<el-checkbox v-model="dialogData.rowData!.hasAlert" :label="$t('alert.isAlert')" />
|
||||
<span class="input-help">{{ $t('alert.cronJobHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="alertCount"
|
||||
v-if="dialogData.rowData!.hasAlert && isProductPro"
|
||||
:label="$t('alert.alertCount')"
|
||||
>
|
||||
<el-input-number
|
||||
style="width: 200px"
|
||||
:min="1"
|
||||
step-strictly
|
||||
:step="1"
|
||||
v-model.number="dialogData.rowData!.alertCount"
|
||||
></el-input-number>
|
||||
<span class="input-help">{{ $t('alert.alertCountHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogData.rowData!.hasAlert && !isProductPro">
|
||||
<span>{{ $t('alert.licenseHelper') }}</span>
|
||||
<el-button link type="primary" @click="toUpload">
|
||||
{{ $t('license.levelUpPro') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('cronjob.retainCopies')" prop="retainCopies">
|
||||
<el-input-number
|
||||
style="width: 200px"
|
||||
@ -493,6 +518,7 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
<LicenseImport ref="licenseRef" />
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
@ -522,8 +548,16 @@ import {
|
||||
weekOptions,
|
||||
} from './../helper';
|
||||
import { loadUsers } from '@/api/modules/toolbox';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { GlobalStore } from '@/store';
|
||||
import LicenseImport from '@/components/license-import/index.vue';
|
||||
const router = useRouter();
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const licenseRef = ref();
|
||||
const { isProductPro } = storeToRefs(globalStore);
|
||||
const alertTypes = ['app', 'website', 'database', 'directory', 'log', 'snapshot'];
|
||||
|
||||
interface DialogProps {
|
||||
title: string;
|
||||
rowData?: Cronjob.CronjobInfo;
|
||||
@ -563,13 +597,6 @@ const acceptParams = (params: DialogProps): void => {
|
||||
dialogData.value.rowData.dbType = 'mysql';
|
||||
dialogData.value.rowData.isDir = true;
|
||||
}
|
||||
if (dialogData.value.rowData.sourceAccountIDs) {
|
||||
dialogData.value.rowData.sourceAccounts = [];
|
||||
let itemIDs = dialogData.value.rowData.sourceAccountIDs.split(',');
|
||||
for (const item of itemIDs) {
|
||||
dialogData.value.rowData.sourceAccounts.push(Number(item));
|
||||
}
|
||||
}
|
||||
dialogData.value.rowData!.command = dialogData.value.rowData!.command || 'sh';
|
||||
dialogData.value.rowData!.isCustom =
|
||||
dialogData.value.rowData!.command !== 'sh' &&
|
||||
@ -732,6 +759,17 @@ const verifyFiles = (rule: any, value: any, callback: any) => {
|
||||
callback();
|
||||
};
|
||||
|
||||
const checkSendCount = (rule: any, value: any, callback: any) => {
|
||||
if (value === '') {
|
||||
callback();
|
||||
}
|
||||
const regex = /^(?:[1-9]|[12][0-9]|30)$/;
|
||||
if (!regex.test(value)) {
|
||||
return callback(new Error(i18n.global.t('commons.rule.numberRange', [1, 30])));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.noSpace],
|
||||
type: [Rules.requiredSelect],
|
||||
@ -750,6 +788,7 @@ const rules = reactive({
|
||||
sourceAccounts: [Rules.requiredSelect],
|
||||
downloadAccountID: [Rules.requiredSelect],
|
||||
retainCopies: [Rules.number],
|
||||
alertCount: [Rules.integerNumber, { validator: checkSendCount, trigger: 'blur' }],
|
||||
});
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
@ -980,7 +1019,18 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (dialogData.value?.rowData?.exclusionRules) {
|
||||
dialogData.value.rowData.exclusionRules = dialogData.value.rowData.exclusionRules.replaceAll('\n', ',');
|
||||
}
|
||||
|
||||
dialogData.value.rowData.alertCount =
|
||||
dialogData.value.rowData!.hasAlert && isProductPro.value ? dialogData.value.rowData.alertCount : 0;
|
||||
dialogData.value.rowData.alertTitle =
|
||||
dialogData.value.rowData!.hasAlert && isProductPro.value
|
||||
? i18n.global.t('cronjob.alertTitle', [
|
||||
i18n.global.t('cronjob.' + dialogData.value.rowData.type),
|
||||
dialogData.value.rowData.name,
|
||||
])
|
||||
: '';
|
||||
if (!dialogData.value.rowData) return;
|
||||
|
||||
if (dialogData.value.title === 'create') {
|
||||
await addCronjob(dialogData.value.rowData);
|
||||
}
|
||||
@ -994,6 +1044,10 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
const toUpload = () => {
|
||||
licenseRef.value.acceptParams();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
@ -4,18 +4,20 @@
|
||||
<el-col :span="20" :offset="2" v-if="open">
|
||||
<el-alert
|
||||
type="error"
|
||||
:description="$t('app.deleteHelper', [$t('app.database')])"
|
||||
:title="$t('app.deleteHelper', [$t('app.database')])"
|
||||
center
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<br />
|
||||
<el-descriptions border :column="1">
|
||||
<el-descriptions-item>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label-class-name="check-label" class-name="check-content" min-width="60px">
|
||||
<template #label>
|
||||
<a href="javascript:void(0);" @click="toApp()">{{ $t('app.app') }}</a>
|
||||
<a href="javascript:void(0);" class="check-label-a" @click="toApp()">
|
||||
{{ $t('app.app') }}
|
||||
</a>
|
||||
</template>
|
||||
{{ installData.join(',') }}
|
||||
<pre>{{ installData.join('\n') }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
|
@ -236,11 +236,12 @@
|
||||
</div>
|
||||
|
||||
<DialogPro v-model="open" :title="$t('app.checkTitle')" size="small">
|
||||
<el-alert :closable="false" :title="$t('app.checkInstalledWarn', [dashboardName])" type="info">
|
||||
<div class="flex justify-center items-center gap-2 flex-wrap">
|
||||
{{ $t('app.checkInstalledWarn', [dashboardName]) }}
|
||||
<el-link icon="Position" @click="getAppDetail" type="primary">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-link>
|
||||
</el-alert>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
|
@ -10,12 +10,14 @@
|
||||
:closable="false"
|
||||
/>
|
||||
<br />
|
||||
<el-descriptions border :column="1">
|
||||
<el-descriptions-item>
|
||||
<el-descriptions border :column="1" class="mt-5">
|
||||
<el-descriptions-item label-class-name="check-label" class-name="check-content" min-width="60px">
|
||||
<template #label>
|
||||
<a href="javascript:void(0);" @click="toApp()">{{ $t('app.app') }}</a>
|
||||
<a href="javascript:void(0);" class="check-label-a" @click="toApp()">
|
||||
{{ $t('app.app') }}
|
||||
</a>
|
||||
</template>
|
||||
{{ installData.join(',') }}
|
||||
<pre>{{ installData.join('\n') }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
|
@ -200,11 +200,12 @@
|
||||
</div>
|
||||
|
||||
<DialogPro v-model="open" :title="$t('app.checkTitle')" size="small">
|
||||
<el-alert :closable="false" :title="$t('app.checkInstalledWarn', [dashboardName])" type="info">
|
||||
<div class="flex justify-center items-center gap-2 flex-wrap">
|
||||
{{ $t('app.checkInstalledWarn', [dashboardName]) }}
|
||||
<el-link icon="Position" @click="getAppDetail" type="primary">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-link>
|
||||
</el-alert>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
|
@ -10,12 +10,14 @@
|
||||
:closable="false"
|
||||
/>
|
||||
<br />
|
||||
<el-descriptions border :column="1">
|
||||
<el-descriptions-item>
|
||||
<el-descriptions border :column="1" class="mt-5">
|
||||
<el-descriptions-item label-class-name="check-label" class-name="check-content" min-width="60px">
|
||||
<template #label>
|
||||
<a href="javascript:void(0);" @click="toApp()">{{ $t('app.app') }}</a>
|
||||
<a href="javascript:void(0);" class="check-label-a" @click="toApp()">
|
||||
{{ $t('app.app') }}
|
||||
</a>
|
||||
</template>
|
||||
{{ installData.join(',') }}
|
||||
<pre>{{ installData.join('\n') }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
|
@ -120,11 +120,12 @@
|
||||
<Conn ref="connRef" @check-exist="reOpenTerminal" @close-terminal="closeTerminal(true)" />
|
||||
|
||||
<DialogPro v-model="open" :title="$t('app.checkTitle')" size="small">
|
||||
<el-alert :closable="false" :title="$t('app.checkInstalledWarn', ['Redis-Commander'])" type="info">
|
||||
<div class="flex justify-center items-center gap-2 flex-wrap">
|
||||
{{ $t('app.checkInstalledWarn', ['Redis-Commander']) }}
|
||||
<el-link icon="Position" @click="getAppDetail('redis-commander')" type="primary">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-link>
|
||||
</el-alert>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
|
@ -316,7 +316,7 @@ defineExpose({
|
||||
.h-app-title {
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
color: #1f2329;
|
||||
color: var(--panel-text-color);
|
||||
}
|
||||
|
||||
.h-app-desc {
|
||||
|
@ -599,7 +599,7 @@ onBeforeUnmount(() => {
|
||||
.system-label {
|
||||
font-weight: 400 !important;
|
||||
font-size: 14px !important;
|
||||
color: #1f2329;
|
||||
color: var(--panel-text-color);
|
||||
}
|
||||
|
||||
.system-content {
|
||||
|
@ -58,7 +58,7 @@
|
||||
</div>
|
||||
<div v-loading="loading">
|
||||
<div class="flex">
|
||||
<div class="sm:w-48 w-1/3 monaco-editor-background tree-container" v-if="isShow">
|
||||
<div class="monaco-editor sm:w-48 w-1/3 monaco-editor-background border-0 tree-container" v-if="isShow">
|
||||
<div class="flex items-center justify-between pl-1 sm:pr-4 pr-1 pt-1">
|
||||
<el-tooltip :content="$t('file.top')" placement="top">
|
||||
<el-text size="small" @click="getUpData()" class="cursor-pointer">
|
||||
@ -702,6 +702,11 @@ defineExpose({ acceptParams });
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
.monaco-editor-background {
|
||||
outline-style: none;
|
||||
background-color: var(--vscode-editor-background) !important;
|
||||
}
|
||||
|
||||
.tree-widget {
|
||||
background-color: var(--el-button--primary);
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ import { File } from '@/api/interface/file';
|
||||
import { getIcon } from '@/utils/util';
|
||||
import { DeleteFile, GetRecycleStatus } from '@/api/modules/files';
|
||||
import { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
|
||||
const open = ref(false);
|
||||
const files = ref();
|
||||
@ -85,13 +86,20 @@ const getStatus = async () => {
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
const onConfirm = async () => {
|
||||
const pros = [];
|
||||
for (const s of files.value) {
|
||||
if (s['isDir']) {
|
||||
if (s['path'].indexOf('.1panel_clash') > -1) {
|
||||
MsgWarning(i18n.global.t('file.clashDeleteAlert'));
|
||||
return;
|
||||
}
|
||||
const pathRes = await loadBaseDir();
|
||||
if (s['path'] === pathRes.data) {
|
||||
MsgWarning(i18n.global.t('file.panelInstallDir'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
pros.push(DeleteFile({ path: s['path'], isDir: s['isDir'], forceDelete: forceDelete.value }));
|
||||
}
|
||||
loading.value = true;
|
||||
|
@ -316,7 +316,6 @@ import { StarFilled, Star, Top, Right, Close } from '@element-plus/icons-vue';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { Mimetypes, Languages } from '@/global/mimetype';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Back, Refresh } from '@element-plus/icons-vue';
|
||||
import { MsgWarning } from '@/utils/message';
|
||||
import { useSearchable } from './hooks/searchable';
|
||||
import { ResultData } from '@/api/interface';
|
||||
@ -532,41 +531,22 @@ const top = () => {
|
||||
};
|
||||
|
||||
const jump = async (url: string) => {
|
||||
const fileName = url.substring(url.lastIndexOf('/') + 1);
|
||||
let filePath = url.substring(0, url.lastIndexOf('/') + 1);
|
||||
if (!url.includes('.')) {
|
||||
filePath = url;
|
||||
}
|
||||
history.splice(pointer + 1);
|
||||
history.push(url);
|
||||
pointer = history.length - 1;
|
||||
|
||||
const oldUrl = req.path;
|
||||
const oldPageSize = req.pageSize;
|
||||
// reset search params before exec jump
|
||||
Object.assign(req, initData());
|
||||
req.path = url;
|
||||
req.path = filePath;
|
||||
req.containSub = false;
|
||||
req.search = '';
|
||||
req.pageSize = oldPageSize;
|
||||
const { path: oldUrl, pageSize: oldPageSize } = req;
|
||||
Object.assign(req, initData(), { path: url, containSub: false, search: '', pageSize: oldPageSize });
|
||||
let searchResult = await searchFile();
|
||||
|
||||
globalStore.setLastFilePath(req.path);
|
||||
// check search result,the file is exists?
|
||||
if (!searchResult.data.path) {
|
||||
req.path = oldUrl;
|
||||
globalStore.setLastFilePath(req.path);
|
||||
MsgWarning(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
if (fileName && fileName.length > 1 && fileName.includes('.')) {
|
||||
const fileData = searchResult.data.items.filter((item) => item.name === fileName);
|
||||
if (fileData && fileData.length === 1) {
|
||||
openView(fileData[0]);
|
||||
} else {
|
||||
MsgWarning(i18n.global.t('commons.res.notFound'));
|
||||
}
|
||||
}
|
||||
req.path = searchResult.data.path;
|
||||
globalStore.setLastFilePath(req.path);
|
||||
handleSearchResult(searchResult);
|
||||
getPaths(req.path);
|
||||
nextTick(function () {
|
||||
@ -701,11 +681,16 @@ const openView = (item: File.File) => {
|
||||
text: () => openCodeEditor(item.path, item.extension),
|
||||
};
|
||||
|
||||
return actionMap[fileType] ? actionMap[fileType](item) : openCodeEditor(item.path, item.extension);
|
||||
const path = item.isSymlink ? item.linkPath : item.path;
|
||||
return actionMap[fileType] ? actionMap[fileType](item) : openCodeEditor(path, item.extension);
|
||||
};
|
||||
|
||||
const openPreview = (item: File.File, fileType: string) => {
|
||||
filePreview.path = item.path;
|
||||
if (item.mode.toString() == '-' && item.user == '-' && item.group == '-') {
|
||||
MsgWarning(i18n.global.t('file.fileCanNotRead'));
|
||||
return;
|
||||
}
|
||||
filePreview.path = item.isSymlink ? item.linkPath : item.path;
|
||||
filePreview.name = item.name;
|
||||
filePreview.extension = item.extension;
|
||||
filePreview.fileType = fileType;
|
||||
@ -906,7 +891,7 @@ const buttons = [
|
||||
label: i18n.global.t('file.deCompress'),
|
||||
click: openDeCompress,
|
||||
disabled: (row: File.File) => {
|
||||
return row.isDir;
|
||||
return !isDecompressFile(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -946,6 +931,20 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
|
||||
const isDecompressFile = (row: File.File) => {
|
||||
if (row.isDir) {
|
||||
return false;
|
||||
}
|
||||
if (getFileType(row.extension) === 'compress') {
|
||||
return true;
|
||||
}
|
||||
if (row.mimeType == 'application/octet-stream') {
|
||||
return false;
|
||||
} else {
|
||||
return Mimetypes.get(row.mimeType) != undefined;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (router.currentRoute.value.query.path) {
|
||||
req.path = String(router.currentRoute.value.query.path);
|
||||
|
@ -115,7 +115,9 @@ const acceptParams = (props: EditProps) => {
|
||||
isFullscreen.value = fileType.value === 'excel';
|
||||
|
||||
loading.value = true;
|
||||
fileUrl.value = `${import.meta.env.VITE_API_URL as string}/files/download?path=${encodeURIComponent(props.path)}`;
|
||||
fileUrl.value = `${import.meta.env.VITE_API_URL as string}/files/download?path=${encodeURIComponent(
|
||||
props.path,
|
||||
)}×tamp=${new Date().getTime()}`;
|
||||
open.value = true;
|
||||
loading.value = false;
|
||||
};
|
||||
|
@ -162,6 +162,7 @@ const replacements = {
|
||||
'[MFAStatus]': 'setting.mfa',
|
||||
'[MonitorStatus]': 'setting.enableMonitor',
|
||||
'[MonitorStoreDays]': 'setting.monitor',
|
||||
'[ApiInterfaceStatus]': 'setting.apiInterface',
|
||||
};
|
||||
|
||||
const onSubmitClean = async () => {
|
||||
@ -174,3 +175,12 @@ onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-button {
|
||||
&.no-active {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -366,7 +366,8 @@ const loadDataFromDB = async () => {
|
||||
globalStore.entrance = res.data.securityEntrance;
|
||||
globalStore.setOpenMenuTabs(res.data.menuTabs === 'enable');
|
||||
globalStore.updateLanguage(res.data.language);
|
||||
globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme, panelName: res.data.panelName });
|
||||
let theme = globalStore.themeConfig.theme === res.data.theme ? res.data.theme : globalStore.themeConfig.theme;
|
||||
globalStore.setThemeConfig({ ...themeConfig.value, theme: theme, panelName: res.data.panelName });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -475,7 +476,11 @@ onMounted(() => {
|
||||
height: 45px;
|
||||
margin-top: 10px;
|
||||
background-color: #005eeb;
|
||||
border-color: #005eeb;
|
||||
color: #ffffff;
|
||||
&:hover {
|
||||
--el-button-hover-border-color: #005eeb;
|
||||
}
|
||||
}
|
||||
|
||||
.demo {
|
||||
@ -502,6 +507,13 @@ onMounted(() => {
|
||||
color: #005eeb;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: #005eeb;
|
||||
&:hover {
|
||||
color: #005eeb95;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input .el-checkbox__inner) {
|
||||
background-color: #fff !important;
|
||||
border-color: #fff !important;
|
||||
@ -521,5 +533,26 @@ onMounted(() => {
|
||||
margin-top: -20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
:deep(.el-input__inner) {
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
outline: none;
|
||||
}
|
||||
.el-dropdown:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
.el-tooltip__trigger:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
|
||||
color: #005eeb !important;
|
||||
background-color: #e5eefd !important;
|
||||
}
|
||||
:deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
|
||||
color: #005eeb !important;
|
||||
background-color: #e5eefd !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -4,7 +4,11 @@
|
||||
<template #main>
|
||||
<div style="text-align: center; margin-top: 20px">
|
||||
<div style="justify-self: center" class="logo">
|
||||
<img v-if="globalStore.themeConfig.logo" style="width: 80px" :src="'/api/v2/images/logo'" />
|
||||
<img
|
||||
v-if="globalStore.themeConfig.logo"
|
||||
style="width: 80px"
|
||||
:src="`/api/v2/images/logo?t=${Date.now()}`"
|
||||
/>
|
||||
<PrimaryLogo v-else />
|
||||
</div>
|
||||
<h3 class="description">{{ globalStore.themeConfig.title || $t('setting.description') }}</h3>
|
||||
|
@ -93,6 +93,7 @@ import { dateFormat } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { initFavicon } from '@/utils/xpack';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const loading = ref();
|
||||
@ -146,7 +147,7 @@ const onUnbind = async (row: any) => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
if (row.freeCount !== 0) {
|
||||
globalStore.isProductPro = false;
|
||||
globalStore.themeConfig.isGold = false;
|
||||
initFavicon();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
@ -259,21 +260,3 @@ onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.h-app-card {
|
||||
padding: 10px 15px;
|
||||
margin-right: 10px;
|
||||
line-height: 18px;
|
||||
&:hover {
|
||||
background-color: rgba(0, 94, 235, 0.03);
|
||||
}
|
||||
}
|
||||
:deep(.el-descriptions__content) {
|
||||
max-width: 300px;
|
||||
}
|
||||
.descriptions {
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
175
frontend/src/views/setting/panel/api-interface/index.vue
Normal file
175
frontend/src/views/setting/panel/api-interface/index.vue
Normal file
@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@close="handleClose"
|
||||
size="35%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('setting.apiInterface')" :back="handleClose" />
|
||||
</template>
|
||||
<el-alert class="common-prompt" :closable="false" type="warning">
|
||||
<template #default>
|
||||
<ul>
|
||||
<li>
|
||||
<el-text type="danger">{{ $t('setting.apiInterfaceAlert1') }}</el-text>
|
||||
</li>
|
||||
<li>
|
||||
<el-text type="danger">{{ $t('setting.apiInterfaceAlert2') }}</el-text>
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('setting.apiInterfaceAlert3') }}
|
||||
<el-link :href="apiURL" type="success" target="_blank" class="mb-0.5 ml-0.5">
|
||||
{{ apiURL }}
|
||||
</el-link>
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('setting.apiInterfaceAlert4') }}
|
||||
<el-link :href="panelURL" type="success" target="_blank" class="mb-0.5 ml-0.5">
|
||||
{{ panelURL }}
|
||||
</el-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-form
|
||||
:model="form"
|
||||
ref="formRef"
|
||||
@submit.prevent
|
||||
v-loading="loading"
|
||||
label-position="top"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form-item :label="$t('setting.apiKey')" prop="apiKey">
|
||||
<el-input v-model="form.apiKey" readonly>
|
||||
<template #suffix>
|
||||
<CopyButton type="icon" :content="form.apiKey" class="w-30" />
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="resetApiKey()">
|
||||
{{ $t('commons.button.reset') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">{{ $t('setting.apiKeyHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.ipWhiteList')" prop="ipWhiteList">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="$t('setting.ipWhiteListEgs')"
|
||||
:rows="4"
|
||||
v-model="form.ipWhiteList"
|
||||
/>
|
||||
<span class="input-help">{{ $t('setting.ipWhiteListHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="onBind(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { generateApiKey, updateApiConfig } from '@/api/modules/setting';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { ElMessageBox, FormInstance } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
const loading = ref();
|
||||
const drawerVisible = ref();
|
||||
const formRef = ref();
|
||||
const apiURL = `${window.location.protocol}//${window.location.hostname}${
|
||||
window.location.port ? `:${window.location.port}` : ''
|
||||
}/1panel/swagger/index.html`;
|
||||
const panelURL = `https://1panel.cn/docs`;
|
||||
const form = reactive({
|
||||
apiKey: '',
|
||||
ipWhiteList: '',
|
||||
apiInterfaceStatus: '',
|
||||
});
|
||||
const rules = reactive({
|
||||
ipWhiteList: [Rules.requiredInput],
|
||||
apiKey: [Rules.requiredInput],
|
||||
});
|
||||
interface DialogProps {
|
||||
apiInterfaceStatus: string;
|
||||
apiKey: string;
|
||||
ipWhiteList: string;
|
||||
}
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
form.apiInterfaceStatus = params.apiInterfaceStatus;
|
||||
form.apiKey = params.apiKey;
|
||||
if (params.apiKey == '') {
|
||||
await generateApiKey().then((res) => {
|
||||
form.apiKey = res.data;
|
||||
});
|
||||
}
|
||||
form.ipWhiteList = params.ipWhiteList;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const resetApiKey = async () => {
|
||||
loading.value = true;
|
||||
ElMessageBox.confirm(i18n.global.t('setting.apiKeyResetHelper'), i18n.global.t('setting.apiKeyReset'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
})
|
||||
.then(async () => {
|
||||
await generateApiKey()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
form.apiKey = res.data;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
const onBind = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
let param = {
|
||||
apiKey: form.apiKey,
|
||||
ipWhiteList: form.ipWhiteList,
|
||||
apiInterfaceStatus: form.apiInterfaceStatus,
|
||||
};
|
||||
loading.value = true;
|
||||
await updateApiConfig(param)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleClose = () => {
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -27,10 +27,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('setting.theme')" prop="theme">
|
||||
<div class="flex justify-between items-center gap-6">
|
||||
<el-radio-group @change="onSave('Theme', form.theme)" v-model="form.theme">
|
||||
<el-radio-button v-if="isMasterProductPro" value="dark-gold">
|
||||
<span>{{ $t('setting.darkGold') }}</span>
|
||||
</el-radio-button>
|
||||
<el-radio-button value="light">
|
||||
<span>{{ $t('setting.light') }}</span>
|
||||
</el-radio-button>
|
||||
@ -41,6 +39,15 @@
|
||||
<span>{{ $t('setting.auto') }}</span>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button
|
||||
v-if="isMasterProductPro"
|
||||
@click="onChangeThemeColor"
|
||||
icon="Setting"
|
||||
class="!h-[34px]"
|
||||
>
|
||||
<span>{{ $t('container.custom') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('setting.menuTabs')" prop="menuTabs">
|
||||
@ -99,6 +106,16 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('setting.apiInterface')" prop="apiInterface">
|
||||
<el-switch
|
||||
@change="onChangeApiInterfaceStatus"
|
||||
v-model="form.apiInterfaceStatus"
|
||||
active-value="enable"
|
||||
inactive-value="disable"
|
||||
/>
|
||||
<span class="input-help">{{ $t('setting.apiInterfaceHelper') }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('setting.developerMode')" prop="developerMode">
|
||||
<el-radio-group
|
||||
@change="onSave('DeveloperMode', form.developerMode)"
|
||||
@ -133,19 +150,23 @@
|
||||
<UserName ref="userNameRef" />
|
||||
<PanelName ref="panelNameRef" @search="search()" />
|
||||
<Proxy ref="proxyRef" @search="search()" />
|
||||
<ApiInterface ref="apiInterfaceRef" @search="search()" />
|
||||
<Timeout ref="timeoutRef" @search="search()" />
|
||||
<HideMenu ref="hideMenuRef" @search="search()" />
|
||||
<ThemeColor ref="themeColorRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { ElForm, ElMessageBox } from 'element-plus';
|
||||
import { getSettingInfo, updateSetting, getSystemAvailable } from '@/api/modules/setting';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useTheme } from '@/global/use-theme';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import ThemeColor from '@/views/setting/panel/theme-color/index.vue';
|
||||
import ApiInterface from '@/views/setting/panel/api-interface/index.vue';
|
||||
import Password from '@/views/setting/panel/password/index.vue';
|
||||
import UserName from '@/views/setting/panel/username/index.vue';
|
||||
import Timeout from '@/views/setting/panel/timeout/index.vue';
|
||||
@ -154,6 +175,7 @@ import Proxy from '@/views/setting/panel/proxy/index.vue';
|
||||
import HideMenu from '@/views/setting/panel/hidemenu/index.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getXpackSetting, updateXpackSettingByKey } from '@/utils/xpack';
|
||||
import { setPrimaryColor } from '@/utils/theme';
|
||||
|
||||
const loading = ref(false);
|
||||
const i18n = useI18n();
|
||||
@ -166,12 +188,18 @@ const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
|
||||
interface ThemeColor {
|
||||
light: string;
|
||||
dark: string;
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
userName: '',
|
||||
password: '',
|
||||
sessionTimeout: 0,
|
||||
panelName: '',
|
||||
theme: '',
|
||||
themeColor: {} as ThemeColor,
|
||||
menuTabs: '',
|
||||
language: '',
|
||||
complexityVerification: '',
|
||||
@ -184,6 +212,11 @@ const form = reactive({
|
||||
proxyUser: '',
|
||||
proxyPasswd: '',
|
||||
proxyPasswdKeep: '',
|
||||
proxyDocker: '',
|
||||
|
||||
apiInterfaceStatus: 'disable',
|
||||
apiKey: '',
|
||||
ipWhiteList: '',
|
||||
|
||||
proHideMenus: ref(i18n.t('setting.unSetting')),
|
||||
hideMenuList: '',
|
||||
@ -197,6 +230,8 @@ const panelNameRef = ref();
|
||||
const proxyRef = ref();
|
||||
const timeoutRef = ref();
|
||||
const hideMenuRef = ref();
|
||||
const themeColorRef = ref();
|
||||
const apiInterfaceRef = ref();
|
||||
const unset = ref(i18n.t('setting.unSetting'));
|
||||
|
||||
interface Node {
|
||||
@ -226,6 +261,9 @@ const search = async () => {
|
||||
form.proxyUser = res.data.proxyUser;
|
||||
form.proxyPasswd = res.data.proxyPasswd;
|
||||
form.proxyPasswdKeep = res.data.proxyPasswdKeep;
|
||||
form.apiInterfaceStatus = res.data.apiInterfaceStatus;
|
||||
form.apiKey = res.data.apiKey;
|
||||
form.ipWhiteList = res.data.ipWhiteList;
|
||||
|
||||
const json: Node = JSON.parse(res.data.xpackHideMenu);
|
||||
if (json.isCheck === false) {
|
||||
@ -243,11 +281,15 @@ const search = async () => {
|
||||
if (isMasterProductPro.value) {
|
||||
const xpackRes = await getXpackSetting();
|
||||
if (xpackRes) {
|
||||
form.theme = xpackRes.data.theme === 'dark-gold' ? 'dark-gold' : res.data.theme;
|
||||
return;
|
||||
form.theme = xpackRes.data.theme || globalStore.themeConfig.theme || 'light';
|
||||
form.themeColor = JSON.parse(xpackRes.data.themeColor);
|
||||
globalStore.themeConfig.themeColor = xpackRes.data.themeColor;
|
||||
globalStore.themeConfig.theme = form.theme;
|
||||
form.proxyDocker = xpackRes.data.proxyDocker;
|
||||
}
|
||||
} else {
|
||||
form.theme = globalStore.themeConfig.theme || res.data.theme || 'light';
|
||||
}
|
||||
form.theme = res.data.theme;
|
||||
};
|
||||
|
||||
function extractTitles(node: Node, result: string[]): void {
|
||||
@ -294,6 +336,7 @@ const onChangeProxy = () => {
|
||||
user: form.proxyUser,
|
||||
passwd: form.proxyPasswd,
|
||||
passwdKeep: form.proxyPasswdKeep,
|
||||
proxyDocker: form.proxyDocker,
|
||||
});
|
||||
};
|
||||
|
||||
@ -301,6 +344,46 @@ const onChangeHideMenus = () => {
|
||||
hideMenuRef.value.acceptParams({ menuList: form.hideMenuList });
|
||||
};
|
||||
|
||||
const onChangeThemeColor = () => {
|
||||
const themeColor: ThemeColor = JSON.parse(globalStore.themeConfig.themeColor);
|
||||
themeColorRef.value.acceptParams({ themeColor: themeColor, theme: globalStore.themeConfig.theme });
|
||||
};
|
||||
|
||||
const onChangeApiInterfaceStatus = () => {
|
||||
if (form.apiInterfaceStatus === 'enable') {
|
||||
apiInterfaceRef.value.acceptParams({
|
||||
apiInterfaceStatus: form.apiInterfaceStatus,
|
||||
apiKey: form.apiKey,
|
||||
ipWhiteList: form.ipWhiteList,
|
||||
});
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm(i18n.t('setting.apiInterfaceClose'), i18n.t('setting.apiInterface'), {
|
||||
confirmButtonText: i18n.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.t('commons.button.cancel'),
|
||||
})
|
||||
.then(async () => {
|
||||
loading.value = true;
|
||||
await updateSetting({ key: 'ApiInterfaceStatus', value: 'disable' })
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
search();
|
||||
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
apiInterfaceRef.value.acceptParams({
|
||||
apiInterfaceStatus: 'enable',
|
||||
apiKey: form.apiKey,
|
||||
ipWhiteList: form.ipWhiteList,
|
||||
});
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
const onSave = async (key: string, val: any) => {
|
||||
loading.value = true;
|
||||
if (key === 'Language') {
|
||||
@ -308,21 +391,20 @@ const onSave = async (key: string, val: any) => {
|
||||
globalStore.updateLanguage(val);
|
||||
}
|
||||
if (key === 'Theme') {
|
||||
if (val === 'dark-gold') {
|
||||
globalStore.themeConfig.isGold = true;
|
||||
} else {
|
||||
globalStore.themeConfig.isGold = false;
|
||||
globalStore.themeConfig.theme = val;
|
||||
}
|
||||
switchTheme();
|
||||
if (globalStore.isMasterProductPro) {
|
||||
updateXpackSettingByKey('Theme', val === 'dark-gold' ? 'dark-gold' : '');
|
||||
if (val === 'dark-gold') {
|
||||
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
|
||||
loading.value = false;
|
||||
search();
|
||||
return;
|
||||
await updateXpackSettingByKey('Theme', val);
|
||||
let color: string;
|
||||
const themeColor: ThemeColor = JSON.parse(globalStore.themeConfig.themeColor);
|
||||
if (val === 'auto') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
color = prefersDark.matches ? themeColor.dark : themeColor.light;
|
||||
} else {
|
||||
color = val === 'dark' ? themeColor.dark : themeColor.light;
|
||||
}
|
||||
globalStore.themeConfig.primary = color;
|
||||
setPrimaryColor(color);
|
||||
}
|
||||
}
|
||||
if (key === 'MenuTabs') {
|
||||
|
@ -7,6 +7,7 @@
|
||||
<ul class="-ml-5">
|
||||
<li v-if="isMasterProductPro">{{ $t('setting.proxyHelper1') }}</li>
|
||||
<li v-if="isMasterProductPro">{{ $t('setting.proxyHelper2') }}</li>
|
||||
<li v-if="isMasterProductPro">{{ $t('setting.proxyHelper4') }}</li>
|
||||
<li>{{ $t('setting.proxyHelper3') }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
@ -44,6 +45,10 @@
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.proxyPasswdKeepItem" :label="$t('setting.proxyPasswdKeep')" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isMasterProductPro">
|
||||
<el-checkbox v-model="form.proxyDocker" :label="$t('setting.proxyDocker')" />
|
||||
<span class="input-help">{{ $t('setting.proxyDockerHelper') }}</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@ -55,6 +60,8 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
|
||||
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -66,11 +73,16 @@ import { reactive, ref } from 'vue';
|
||||
import { updateProxy } from '@/api/modules/setting';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { updateXpackSettingByKey } from '@/utils/xpack';
|
||||
import { updateDaemonJson } from '@/api/modules/container';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import { escapeProxyURL } from '@/utils/util';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
const { isMasterProductPro } = storeToRefs(globalStore);
|
||||
|
||||
const confirmDialogRef = ref();
|
||||
const formRef = ref<FormInstance>();
|
||||
const rules = reactive({
|
||||
proxyType: [Rules.requiredSelect],
|
||||
@ -80,6 +92,7 @@ const rules = reactive({
|
||||
|
||||
const loading = ref(false);
|
||||
const passwordVisible = ref<boolean>(false);
|
||||
const proxyDockerVisible = ref<boolean>(false);
|
||||
const form = reactive({
|
||||
proxyUrl: '',
|
||||
proxyType: '',
|
||||
@ -89,6 +102,7 @@ const form = reactive({
|
||||
proxyPasswd: '',
|
||||
proxyPasswdKeep: '',
|
||||
proxyPasswdKeepItem: false,
|
||||
proxyDocker: false,
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
@ -98,6 +112,7 @@ interface DialogProps {
|
||||
user: string;
|
||||
passwd: string;
|
||||
passwdKeep: string;
|
||||
proxyDocker: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
if (params.url) {
|
||||
@ -113,6 +128,8 @@ const acceptParams = (params: DialogProps): void => {
|
||||
form.proxyPortItem = params.port ? Number(params.port) : 7890;
|
||||
form.proxyUser = params.user;
|
||||
form.proxyPasswd = params.passwd;
|
||||
form.proxyDocker = params.proxyDocker !== '';
|
||||
proxyDockerVisible.value = params.proxyDocker !== '';
|
||||
passwordVisible.value = true;
|
||||
form.proxyPasswdKeepItem = params.passwdKeep === 'Enable';
|
||||
};
|
||||
@ -129,6 +146,7 @@ const submitChangePassword = async (formEl: FormInstance | undefined) => {
|
||||
proxyUser: isClose ? '' : form.proxyUser,
|
||||
proxyPasswd: isClose ? '' : form.proxyPasswd,
|
||||
proxyPasswdKeep: '',
|
||||
proxyDocker: isClose ? false : form.proxyDocker,
|
||||
};
|
||||
if (!isClose) {
|
||||
params.proxyPasswdKeep = form.proxyPasswdKeepItem ? 'Enable' : 'Disable';
|
||||
@ -136,19 +154,80 @@ const submitChangePassword = async (formEl: FormInstance | undefined) => {
|
||||
if (form.proxyType === 'http' || form.proxyType === 'https') {
|
||||
params.proxyUrl = form.proxyType + '://' + form.proxyUrl;
|
||||
}
|
||||
if (
|
||||
isMasterProductPro.value &&
|
||||
(params.proxyDocker ||
|
||||
(proxyDockerVisible.value && isClose) ||
|
||||
(proxyDockerVisible.value && !isClose) ||
|
||||
(proxyDockerVisible.value && !params.proxyDocker))
|
||||
) {
|
||||
let confirmParams = {
|
||||
header: i18n.global.t('setting.confDockerProxy'),
|
||||
operationInfo: i18n.global.t('setting.restartNowHelper'),
|
||||
submitInputInfo: i18n.global.t('setting.restartNow'),
|
||||
};
|
||||
confirmDialogRef.value!.acceptParams(confirmParams);
|
||||
} else {
|
||||
loading.value = true;
|
||||
await updateProxy(params)
|
||||
.then(async () => {
|
||||
loading.value = false;
|
||||
emit('search');
|
||||
passwordVisible.value = false;
|
||||
if (isClose) {
|
||||
await updateDaemonJson(`${form.proxyType}-proxy`, '');
|
||||
}
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
let isClose = form.proxyType === '' || form.proxyType === 'close';
|
||||
let params = {
|
||||
proxyType: isClose ? '' : form.proxyType,
|
||||
proxyUrl: isClose ? '' : form.proxyUrl,
|
||||
proxyPort: isClose ? '' : form.proxyPortItem + '',
|
||||
proxyUser: isClose ? '' : form.proxyUser,
|
||||
proxyPasswd: isClose ? '' : form.proxyPasswd,
|
||||
proxyPasswdKeep: '',
|
||||
proxyDocker: isClose ? false : form.proxyDocker,
|
||||
};
|
||||
if (!isClose) {
|
||||
params.proxyPasswdKeep = form.proxyPasswdKeepItem ? 'Enable' : 'Disable';
|
||||
}
|
||||
let proxyPort = params.proxyPort ? `:${params.proxyPort}` : '';
|
||||
let proxyUser = params.proxyUser ? `${escapeProxyURL(params.proxyUser)}` : '';
|
||||
let proxyPasswd = '';
|
||||
if (params.proxyUser) {
|
||||
proxyPasswd = params.proxyPasswd ? `:${escapeProxyURL(params.proxyPasswd)}@` : '@';
|
||||
}
|
||||
let proxyUrl = form.proxyType + '://' + proxyUser + proxyPasswd + form.proxyUrl + proxyPort;
|
||||
if (form.proxyType === 'http' || form.proxyType === 'https') {
|
||||
params.proxyUrl = form.proxyType + '://' + form.proxyUrl;
|
||||
}
|
||||
await updateProxy(params);
|
||||
if (isClose || params.proxyDocker === false) {
|
||||
proxyUrl = '';
|
||||
}
|
||||
await updateXpackSettingByKey('ProxyDocker', proxyUrl);
|
||||
await updateDaemonJson(`${form.proxyType}-proxy`, proxyUrl);
|
||||
emit('search');
|
||||
handleClose();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
passwordVisible.value = false;
|
||||
};
|
||||
|
223
frontend/src/views/setting/panel/theme-color/index.vue
Normal file
223
frontend/src/views/setting/panel/theme-color/index.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
size="30%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('xpack.theme.customColor')" :back="handleClose" />
|
||||
</template>
|
||||
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form-item :label="$t('setting.light')" prop="light">
|
||||
<div class="flex flex-wrap justify-between items-center sm:items-start">
|
||||
<div class="flex gap-1">
|
||||
<el-tooltip :content="$t('xpack.theme.classicBlue')" placement="top">
|
||||
<el-button
|
||||
color="#005eeb"
|
||||
circle
|
||||
dark
|
||||
:class="form.light === '#005eeb' ? 'selected-white' : ''"
|
||||
@click="changeLightColor('#005eeb')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('xpack.theme.freshGreen')" placement="top">
|
||||
<el-button
|
||||
color="#238636"
|
||||
:class="form.light === '#238636' ? 'selected-white' : ''"
|
||||
circle
|
||||
dark
|
||||
@click="changeLightColor('#238636')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-color-picker
|
||||
v-model="form.light"
|
||||
class="ml-4"
|
||||
:predefine="predefineColors"
|
||||
show-alpha
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('setting.dark')" prop="dark">
|
||||
<div class="flex flex-wrap justify-between items-center sm:items-start">
|
||||
<div class="flex flex-wrap justify-between items-center mt-4 sm:mt-0">
|
||||
<div class="flex gap-1">
|
||||
<el-tooltip :content="$t('xpack.theme.classicBlue')" placement="top">
|
||||
<el-button
|
||||
color="#3D8EFF"
|
||||
circle
|
||||
dark
|
||||
:class="form.dark === '#3D8EFF' ? 'selected-white' : ''"
|
||||
@click="changeDarkColor('#3D8EFF')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('xpack.theme.lingXiaGold')" placement="top">
|
||||
<el-button
|
||||
color="#F0BE96"
|
||||
:class="form.dark === '#F0BE96' ? 'selected-white' : ''"
|
||||
circle
|
||||
dark
|
||||
@click="changeDarkColor('#F0BE96')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('xpack.theme.freshGreen')" placement="top">
|
||||
<el-button
|
||||
color="#238636"
|
||||
:class="form.dark === '#238636' ? 'selected-white' : ''"
|
||||
circle
|
||||
dark
|
||||
@click="changeDarkColor('#238636')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-color-picker
|
||||
v-model="form.dark"
|
||||
class="ml-4"
|
||||
:predefine="predefineColors"
|
||||
show-alpha
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onReSet">{{ $t('xpack.theme.setDefault') }}</el-button>
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { initFavicon, updateXpackSettingByKey } from '@/utils/xpack';
|
||||
import { setPrimaryColor } from '@/utils/theme';
|
||||
import { GlobalStore } from '@/store';
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
const drawerVisible = ref();
|
||||
const loading = ref();
|
||||
interface DialogProps {
|
||||
themeColor: { light: string; dark: string };
|
||||
theme: '';
|
||||
}
|
||||
interface ThemeColor {
|
||||
light: string;
|
||||
dark: string;
|
||||
}
|
||||
const form = reactive({
|
||||
themeColor: {} as ThemeColor,
|
||||
theme: '',
|
||||
light: '',
|
||||
dark: '',
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const predefineColors = ref([
|
||||
'#005eeb',
|
||||
'#3D8EFF',
|
||||
'#F0BE96',
|
||||
'#238636',
|
||||
'#00ced1',
|
||||
'#c71585',
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
]);
|
||||
const globalStore = GlobalStore();
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
form.themeColor = params.themeColor;
|
||||
form.theme = params.theme;
|
||||
form.dark = form.themeColor.dark;
|
||||
form.light = form.themeColor.light;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const changeLightColor = (color: string) => {
|
||||
form.light = color;
|
||||
};
|
||||
const changeDarkColor = (color: string) => {
|
||||
form.dark = color;
|
||||
};
|
||||
const onSave = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
ElMessageBox.confirm(i18n.global.t('xpack.theme.setHelper'), i18n.global.t('commons.button.save'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
form.themeColor = { light: form.light, dark: form.dark };
|
||||
if (globalStore.isMasterProductPro) {
|
||||
await updateXpackSettingByKey('ThemeColor', JSON.stringify(form.themeColor));
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
globalStore.themeConfig.themeColor = JSON.stringify(form.themeColor);
|
||||
loading.value = false;
|
||||
let color: string;
|
||||
if (form.theme === 'auto') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
color = prefersDark.matches ? form.dark : form.light;
|
||||
} else {
|
||||
color = form.theme === 'dark' ? form.dark : form.light;
|
||||
}
|
||||
globalStore.themeConfig.primary = color;
|
||||
setPrimaryColor(color);
|
||||
initFavicon();
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const onReSet = async () => {
|
||||
ElMessageBox.confirm(i18n.global.t('xpack.theme.setDefaultHelper'), i18n.global.t('xpack.theme.setDefault'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
form.themeColor = { light: '#005eeb', dark: '#F0BE96' };
|
||||
if (globalStore.isMasterProductPro) {
|
||||
await updateXpackSettingByKey('ThemeColor', JSON.stringify(form.themeColor));
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
loading.value = false;
|
||||
globalStore.themeConfig.themeColor = JSON.stringify(form.themeColor);
|
||||
let color: string;
|
||||
if (form.theme === 'auto') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
color = prefersDark.matches ? '#F0BE96' : '#005eeb';
|
||||
} else {
|
||||
color = form.theme === 'dark' ? '#F0BE96' : '#005eeb';
|
||||
}
|
||||
globalStore.themeConfig.primary = color;
|
||||
setPrimaryColor(color);
|
||||
initFavicon();
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.selected-white {
|
||||
box-shadow: inset 0 0 0 1px white;
|
||||
}
|
||||
</style>
|
@ -6,7 +6,7 @@
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onCreate()">
|
||||
{{ $t('commons.button.create') }} {{ $t('terminal.quickCommand') }}
|
||||
{{ $t('commons.button.create') }}{{ $t('terminal.quickCommand') }}
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="onOpenGroupDialog()">
|
||||
{{ $t('terminal.group') }}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<el-tabs
|
||||
type="card"
|
||||
class="terminal-tabs"
|
||||
style="background-color: #efefef; margin-top: 20px"
|
||||
style="background-color: var(--panel-terminal-tag-bg-color); margin-top: 20px"
|
||||
v-model="terminalValue"
|
||||
:before-leave="beforeLeave"
|
||||
@tab-change="quickCmd = ''"
|
||||
@ -44,7 +44,10 @@
|
||||
</span>
|
||||
</template>
|
||||
<Terminal
|
||||
:style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }"
|
||||
:style="{
|
||||
height: `calc(100vh - ${loadHeight()})`,
|
||||
'background-color': `var(--panel-logs-bg-color)`,
|
||||
}"
|
||||
:ref="'t-' + item.index"
|
||||
:key="item.Refresh"
|
||||
></Terminal>
|
||||
@ -408,7 +411,7 @@ defineExpose({
|
||||
onMounted(() => {
|
||||
if (router.currentRoute.value.query.path) {
|
||||
const path = String(router.currentRoute.value.query.path);
|
||||
initCmd.value = `cd ${path} \n`;
|
||||
initCmd.value = `cd "${path}" \n`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -428,12 +431,17 @@ onMounted(() => {
|
||||
z-index: calc(var(--el-index-normal) + 1);
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
color: #575758;
|
||||
padding: 0 0px;
|
||||
padding: 0;
|
||||
}
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #ebeef5;
|
||||
background-color: #575758;
|
||||
color: var(--panel-terminal-tag-active-text-color);
|
||||
background-color: var(--panel-terminal-tag-active-bg-color);
|
||||
}
|
||||
:deep(.el-tabs__item:hover) {
|
||||
color: var(--panel-terminal-tag-hover-text-color);
|
||||
}
|
||||
:deep(.el-tabs__item.is-active:hover) {
|
||||
color: var(--panel-terminal-tag-active-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,7 +245,7 @@ const toDoc = async () => {
|
||||
};
|
||||
|
||||
const onChange = async (row: any) => {
|
||||
await await updateClam(row);
|
||||
await updateClam(row);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user