1
0
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:
ssongliu 2025-01-15 17:57:33 +08:00
parent bb250557a2
commit 2920aa3588
102 changed files with 2361 additions and 607 deletions

20
agent/app/dto/alert.go Normal file
View 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"`
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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" {

View File

@ -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
View File

@ -0,0 +1,11 @@
package constant
const (
AlertEnable = "Enable"
AlertDisable = "Disable"
AlertSuccess = "Success"
AlertError = "Error"
AlertSyncError = "SyncError"
AlertPushError = "PushError"
AlertPushSuccess = "PushSuccess"
)

View File

@ -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"
)

View File

@ -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!"

View File

@ -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: "告警資訊遠端錯誤,請檢查後重試!"

View File

@ -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: "告警信息远端错误,请检查后重试!"

View File

@ -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
}
}

View File

@ -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") {

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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 = "--"

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`

View File

@ -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")

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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")

View File

@ -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"

View File

@ -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: "名稱已存在"

View File

@ -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: "演示服务器,禁止此操作!"

View File

@ -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)

View File

@ -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
},
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -34,6 +34,9 @@ export namespace Cronjob {
retainCopies: number;
status: string;
secret: string;
hasAlert: boolean;
alertCount: number;
alertTitle: string;
}
export interface Item {
val: string;

View File

@ -11,7 +11,7 @@ export namespace File {
size: number;
isDir: boolean;
isSymlink: boolean;
linkPath: boolean;
linkPath: string;
type: string;
updateTime: string;
modTime: string;

View File

@ -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;

View File

@ -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;

View File

@ -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) => {

View File

@ -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);
};

View File

@ -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 = '';
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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',
},
};

View 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);
};

View File

@ -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 {

View File

@ -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 TimeHours',
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: {

View File

@ -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 解析值',

View File

@ -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: {

View File

@ -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;

View File

@ -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%;

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View 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);
}
}

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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">

View File

@ -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>

View File

@ -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() }}

View File

@ -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

View File

@ -114,3 +114,8 @@ defineExpose({
acceptParams,
});
</script>
<style scoped>
:deep(.log-container) {
background-color: var(--panel-main-bg-color-10);
}
</style>

View File

@ -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,
});

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -316,7 +316,7 @@ defineExpose({
.h-app-title {
font-weight: 500;
font-size: 15px;
color: #1f2329;
color: var(--panel-text-color);
}
.h-app-desc {

View File

@ -599,7 +599,7 @@ onBeforeUnmount(() => {
.system-label {
font-weight: 400 !important;
font-size: 14px !important;
color: #1f2329;
color: var(--panel-text-color);
}
.system-content {

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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,
)}&timestamp=${new Date().getTime()}`;
open.value = true;
loading.value = false;
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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') {

View File

@ -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;
};

View 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>

View File

@ -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') }}

View File

@ -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);
}
}

View File

@ -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