mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-20 16:00:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519a19d507 |
@@ -1,84 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary List quick directories
|
||||
// @Success 200 {array} model.QuickDirectory
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory [get]
|
||||
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
|
||||
items, err := dashboardService.ListQuickDirectories()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, items)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Create quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory [post]
|
||||
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := dashboardService.CreateQuickDirectory(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Update quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory/update [post]
|
||||
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := dashboardService.UpdateQuickDirectory(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Delete quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory/del [post]
|
||||
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -31,27 +31,7 @@ type DashboardBase struct {
|
||||
}
|
||||
|
||||
type ChangeQuicks struct {
|
||||
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type QuickVisibility struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
IsShow bool `json:"isShow"`
|
||||
}
|
||||
|
||||
type QuickDirectoryCreate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
|
||||
type QuickDirectoryDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Legacy bool `json:"legacy"`
|
||||
}
|
||||
|
||||
type QuickDirectoryUpdate struct {
|
||||
QuickDirectoryDelete
|
||||
QuickDirectoryCreate
|
||||
Quicks []QuickJump `json:"quicks"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
|
||||
@@ -5,13 +5,6 @@ type AppLauncher struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type QuickDirectory struct {
|
||||
BaseModel
|
||||
Path string `json:"path"`
|
||||
Alias string `json:"alias"`
|
||||
Legacy bool `json:"legacy" gorm:"-"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -3,7 +3,6 @@ package repo
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LauncherRepo struct{}
|
||||
@@ -18,11 +17,6 @@ type ILauncherRepo interface {
|
||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||
ListQuickJump(withAll bool) []model.QuickJump
|
||||
UpdateQuicks(quicks []model.QuickJump) error
|
||||
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||
CountQuickDirectories() (int64, error)
|
||||
CreateQuickDirectory(item *model.QuickDirectory) error
|
||||
UpdateQuickDirectory(item *model.QuickDirectory) error
|
||||
DeleteQuickDirectory(id uint, legacy bool) error
|
||||
}
|
||||
|
||||
func NewILauncherRepo() ILauncherRepo {
|
||||
@@ -101,73 +95,13 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
||||
for _, item := range quicks {
|
||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"is_show": item.IsShow,
|
||||
"detail": item.Detail,
|
||||
"alias": item.Alias,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// Legacy entries remain in the overview configuration until explicitly deleted.
|
||||
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
|
||||
}
|
||||
|
||||
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||
items := make([]model.QuickDirectory, 0)
|
||||
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var quicks []model.QuickJump
|
||||
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, quick := range quicks {
|
||||
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
|
||||
var count, legacyCount int64
|
||||
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count + legacyCount, nil
|
||||
}
|
||||
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
|
||||
return global.DB.Create(item).Error
|
||||
}
|
||||
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
|
||||
var result *gorm.DB
|
||||
if item.Legacy {
|
||||
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
|
||||
} else {
|
||||
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
|
||||
var result *gorm.DB
|
||||
if legacy {
|
||||
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
|
||||
} else {
|
||||
result = global.DB.Delete(&model.QuickDirectory{}, id)
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -43,10 +43,6 @@ type IDashboardService interface {
|
||||
|
||||
LoadQuickOptions() []dto.QuickJump
|
||||
ChangeQuick(req dto.ChangeQuicks) error
|
||||
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
|
||||
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
|
||||
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
|
||||
|
||||
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
|
||||
ChangeShow(req dto.SettingUpdate) error
|
||||
@@ -401,15 +397,16 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
||||
}
|
||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||
showCount := 0
|
||||
quicks := make([]model.QuickJump, 0, len(req.Quicks))
|
||||
var quicks []model.QuickJump
|
||||
for _, item := range req.Quicks {
|
||||
var quick model.QuickJump
|
||||
if item.IsShow {
|
||||
showCount++
|
||||
}
|
||||
quicks = append(quicks, model.QuickJump{
|
||||
BaseModel: model.BaseModel{ID: item.ID},
|
||||
IsShow: item.IsShow,
|
||||
})
|
||||
if err := copier.Copy(&quick, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
quicks = append(quicks, quick)
|
||||
}
|
||||
if showCount == 0 {
|
||||
return buserr.New("ErrMinQuickJump")
|
||||
@@ -709,13 +706,6 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
quicks := launcherRepo.ListQuickJump(false)
|
||||
for i := 0; i < len(quicks); i++ {
|
||||
switch quicks[i].Name {
|
||||
case "File":
|
||||
count, err := launcherRepo.CountQuickDirectories()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load quick directory count: %v", err)
|
||||
}
|
||||
quicks[i].Detail = fmt.Sprintf("%d", count)
|
||||
quicks[i].Alias = ""
|
||||
case "Agent":
|
||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||
case "Website":
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
)
|
||||
|
||||
func (u *DashboardService) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||
return launcherRepo.ListQuickDirectories()
|
||||
}
|
||||
|
||||
func quickDirectoryValues(req dto.QuickDirectoryCreate) (model.QuickDirectory, error) {
|
||||
item := model.QuickDirectory{Path: req.Path, Alias: strings.TrimSpace(req.Alias)}
|
||||
if strings.TrimSpace(item.Path) == "" {
|
||||
return item, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (u *DashboardService) CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error) {
|
||||
item, err := quickDirectoryValues(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := launcherRepo.CreateQuickDirectory(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (u *DashboardService) UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error) {
|
||||
item, err := quickDirectoryValues(req.QuickDirectoryCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.ID = req.ID
|
||||
item.Legacy = req.Legacy
|
||||
if err := launcherRepo.UpdateQuickDirectory(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (u *DashboardService) DeleteQuickDirectory(req dto.QuickDirectoryDelete) error {
|
||||
return launcherRepo.DeleteQuickDirectory(req.ID, req.Legacy)
|
||||
}
|
||||
@@ -212,10 +212,17 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
websiteSSL.Shell = req.Shell
|
||||
}
|
||||
if req.PushDir {
|
||||
if !files.NewFileOp().Stat(req.Dir) {
|
||||
dirs := certificateDirectories(req.Dir)
|
||||
if len(dirs) == 0 {
|
||||
return nil, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
websiteSSL.Dir = req.Dir
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return nil, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
}
|
||||
websiteSSL.Dir = strings.Join(dirs, "\n")
|
||||
}
|
||||
if req.Domains != "" {
|
||||
domainArray := strings.Split(req.Domains, "\n")
|
||||
@@ -348,7 +355,9 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
workDir = websiteSSL.Dir
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
}
|
||||
logger.Println(i18n.GetMsgByKey("ExecShellStart"))
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
|
||||
@@ -209,11 +209,17 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
|
||||
websiteSSL.Shell = create.Shell
|
||||
}
|
||||
if create.PushDir {
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(create.Dir) {
|
||||
dirs := certificateDirectories(create.Dir)
|
||||
if len(dirs) == 0 {
|
||||
return res, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
websiteSSL.Dir = create.Dir
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return res, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
}
|
||||
websiteSSL.Dir = strings.Join(dirs, "\n")
|
||||
}
|
||||
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
|
||||
|
||||
@@ -573,7 +579,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
workDir = websiteSSL.Dir
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
}
|
||||
printSSLLog(logger, "ExecShellStart", nil)
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
@@ -793,11 +801,16 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
|
||||
}
|
||||
|
||||
if update.PushDir {
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(update.Dir) {
|
||||
_ = fileOP.CreateDir(update.Dir, constant.DirPerm)
|
||||
dirs := certificateDirectories(update.Dir)
|
||||
if len(dirs) == 0 {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
updateParams["dir"] = update.Dir
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
updateParams["dir"] = strings.Join(dirs, "\n")
|
||||
}
|
||||
var domains []string
|
||||
if update.OtherDomains != "" {
|
||||
|
||||
@@ -1529,26 +1529,41 @@ func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// certificateDirectories accepts both legacy single paths and newline-separated paths.
|
||||
func certificateDirectories(value string) []string {
|
||||
var dirs []string
|
||||
seen := make(map[string]bool)
|
||||
for _, dir := range strings.Split(value, "\n") {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
dir = filepath.Clean(dir)
|
||||
if !seen[dir] {
|
||||
seen[dir] = true
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
if websiteSSL.PushDir {
|
||||
fileOp := files.NewFileOp()
|
||||
var (
|
||||
pushErr error
|
||||
MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")}
|
||||
)
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr == nil {
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
if !websiteSSL.PushDir {
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
for _, dir := range certificateDirectories(websiteSSL.Dir) {
|
||||
status := i18n.GetMsgByKey("Success")
|
||||
for _, file := range []struct{ name, content string }{
|
||||
{"privkey.pem", websiteSSL.PrivateKey},
|
||||
{"fullchain.pem", websiteSSL.Pem},
|
||||
} {
|
||||
if err := fileOp.SaveFile(path.Join(dir, file.name), file.content, constant.FilePerm); err != nil {
|
||||
status = i18n.GetMsgByKey("Failed")
|
||||
logger.Printf("Push dir %s failed: %s", dir, err)
|
||||
}
|
||||
}
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", map[string]interface{}{"path": dir, "status": status}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.AddMonitorSettings,
|
||||
migrations.AddVLLMRequestMetrics,
|
||||
migrations.AddVLLMSampleInterval,
|
||||
migrations.AddQuickDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1888,10 +1888,3 @@ var AddVLLMSampleInterval = &gormigrate.Migration{
|
||||
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
|
||||
},
|
||||
}
|
||||
|
||||
var AddQuickDirectory = &gormigrate.Migration{
|
||||
ID: "20261016-add-quick-directory",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.QuickDirectory{})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
|
||||
cmdRouter.GET("/quick/option", baseApi.LoadQuickOption)
|
||||
cmdRouter.POST("/quick/change", baseApi.UpdateQuickJump)
|
||||
cmdRouter.GET("/quick/directory", baseApi.ListQuickDirectories)
|
||||
cmdRouter.POST("/quick/directory", baseApi.CreateQuickDirectory)
|
||||
cmdRouter.POST("/quick/directory/update", baseApi.UpdateQuickDirectory)
|
||||
cmdRouter.POST("/quick/directory/del", baseApi.DeleteQuickDirectory)
|
||||
cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher)
|
||||
cmdRouter.POST("/app/launcher/show", baseApi.UpdateAppLauncher)
|
||||
cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
|
||||
|
||||
@@ -8,12 +8,6 @@ export namespace Dashboard {
|
||||
|
||||
diskSize: number;
|
||||
}
|
||||
export interface QuickDirectory {
|
||||
id: number;
|
||||
legacy?: boolean;
|
||||
path: string;
|
||||
alias: string;
|
||||
}
|
||||
export interface QuickJump {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ export const loadOsInfo = () => {
|
||||
export const loadQuickOption = () => {
|
||||
return http.get<Array<Dashboard.QuickJump>>(`/dashboard/quick/option`);
|
||||
};
|
||||
export const changeQuick = (quicks: Array<Pick<Dashboard.QuickJump, 'id' | 'isShow'>>) => {
|
||||
export const changeQuick = (quicks: Array<Dashboard.QuickJump>) => {
|
||||
return http.post(`/dashboard/quick/change`, { quicks: quicks });
|
||||
};
|
||||
export const loadAppLauncher = () => {
|
||||
@@ -48,11 +48,3 @@ export const loadTopMem = () => {
|
||||
export const systemRestart = (operation: string) => {
|
||||
return http.post(`/dashboard/system/restart/${operation}`);
|
||||
};
|
||||
|
||||
export const loadQuickDirectories = () => http.get<Dashboard.QuickDirectory[]>('/dashboard/quick/directory');
|
||||
export const createQuickDirectory = (data: Omit<Dashboard.QuickDirectory, 'id'>) =>
|
||||
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory', data);
|
||||
export const updateQuickDirectory = (data: Dashboard.QuickDirectory) =>
|
||||
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory/update', data);
|
||||
export const deleteQuickDirectory = (id: number, legacy = false) =>
|
||||
http.post('/dashboard/quick/directory/del', { id, legacy });
|
||||
|
||||
@@ -79,6 +79,16 @@
|
||||
@row-click="handleRowClick"
|
||||
class="cursor-pointer dialog-file-list"
|
||||
>
|
||||
<el-table-column v-if="isMultiple && form.dir" width="48">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
v-if="row.isDir && !row.isCreate"
|
||||
:model-value="selectedPathSet.has(row.path)"
|
||||
@click.stop
|
||||
@change="toggleDirectory(row.path)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" show-overflow-tooltip fix>
|
||||
<template #default="{ row }">
|
||||
<svg-icon
|
||||
@@ -131,6 +141,13 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="file-list-bottom">
|
||||
<el-checkbox
|
||||
v-if="isMultiple && form.dir"
|
||||
:model-value="selectedPathSet.has(req.path)"
|
||||
@change="toggleDirectory(req.path)"
|
||||
>
|
||||
{{ req.path }}
|
||||
</el-checkbox>
|
||||
<div v-if="!isMultiple && selectRow?.path">
|
||||
{{ $t('file.currentSelect') }}
|
||||
<el-tooltip :content="selectRow.path" placement="top">
|
||||
@@ -163,7 +180,11 @@
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="selectFile" :disabled="disBtn">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="selectFile"
|
||||
:disabled="disBtn || (isMultiple && selectRows.length === 0)"
|
||||
>
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@@ -174,7 +195,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { File } from '@/api/interface/file';
|
||||
import { computeDirSize, createFile, getFileContent, getFilesList, searchFavorite } from '@/api/modules/files';
|
||||
import { nextTick, onUpdated, reactive, ref } from 'vue';
|
||||
import { computed, nextTick, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable';
|
||||
@@ -186,6 +207,7 @@ const paths = ref<string[]>([]);
|
||||
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
|
||||
const selectRow = ref({ path: '', name: '' });
|
||||
const selectRows = ref<Array<{ path: string; name: string }>>([]);
|
||||
const selectedPathSet = computed(() => new Set(selectRows.value.map((item) => item.path)));
|
||||
const isMultiple = ref(false);
|
||||
const rowRefs = ref();
|
||||
const open = ref(false);
|
||||
@@ -196,7 +218,7 @@ const favorites = ref<File.Favorite[]>([]);
|
||||
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchableForSelect(paths);
|
||||
const oldUrl = ref<string>('');
|
||||
|
||||
const em = defineEmits(['choose', 'close']);
|
||||
const em = defineEmits(['choose']);
|
||||
|
||||
const form = reactive({
|
||||
path: '/',
|
||||
@@ -211,6 +233,7 @@ interface DialogProps {
|
||||
isAll?: boolean;
|
||||
disabled?: boolean;
|
||||
multiple?: boolean;
|
||||
selectedPaths?: string[];
|
||||
}
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
form.path = props.path || '/';
|
||||
@@ -218,7 +241,8 @@ const acceptParams = (props: DialogProps): void => {
|
||||
form.isAll = props.isAll || false;
|
||||
form.disabled = props.disabled || false;
|
||||
isMultiple.value = props.multiple || false;
|
||||
selectRows.value = [];
|
||||
disBtn.value = false;
|
||||
selectRows.value = [...new Set(props.selectedPaths || [])].map((path) => ({ path, name: path }));
|
||||
selectRow.value = { path: '', name: '' };
|
||||
openPage();
|
||||
req.path = form.path;
|
||||
@@ -245,7 +269,6 @@ const handleClose = () => {
|
||||
open.value = false;
|
||||
selectRow.value = { path: '', name: '' };
|
||||
selectRows.value = [];
|
||||
em('close');
|
||||
};
|
||||
|
||||
const openPage = () => {
|
||||
@@ -282,7 +305,7 @@ const openDir = async (row: File.File, column: any, event: any) => {
|
||||
}
|
||||
// Handle file click
|
||||
const fullPath = (req.path === '/' ? req.path : req.path + '/') + row.name;
|
||||
if (isMultiple.value) {
|
||||
if (isMultiple.value && (form.isAll || !form.dir)) {
|
||||
// Multiple mode: toggle selection
|
||||
const index = selectRows.value.findIndex((r) => r.path === fullPath);
|
||||
if (index > -1) {
|
||||
@@ -474,22 +497,25 @@ const createFolder = async (row: any) => {
|
||||
Object.assign(addItem, addForm);
|
||||
loading.value = true;
|
||||
createFile(addItem as File.FileCreate)
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
row.isCreate = false;
|
||||
disBtn.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
await search(req);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
onUpdated(() => {
|
||||
if (form.path != '') {
|
||||
req.path = form.path;
|
||||
const toggleDirectory = (path: string) => {
|
||||
const index = selectRows.value.findIndex((item) => item.path === path);
|
||||
if (index >= 0) {
|
||||
selectRows.value.splice(index, 1);
|
||||
} else {
|
||||
selectRows.value.push({ path, name: path });
|
||||
}
|
||||
search(req);
|
||||
});
|
||||
};
|
||||
|
||||
const removeSelected = (index: number) => {
|
||||
selectRows.value.splice(index, 1);
|
||||
|
||||
@@ -494,7 +494,6 @@ const message = {
|
||||
recommend: 'Recommended',
|
||||
dir: 'Directory',
|
||||
alias: 'Alias',
|
||||
quickDirDelete: 'Delete ({0})?',
|
||||
quickDir: 'Quick Access',
|
||||
minQuickJump: 'Add at least one quick access entry',
|
||||
maxQuickJump: 'You can add up to four quick access entries',
|
||||
@@ -4253,7 +4252,7 @@ const message = {
|
||||
pushDir: 'Push the certificate to the local directory',
|
||||
dir: 'Directory',
|
||||
pushDirHelper:
|
||||
'Certificate file "fullchain.pem" and key file "privkey.pem" will be generated in this directory.',
|
||||
'Select multiple directories or enter one directory per line. Each directory will receive "fullchain.pem" and "privkey.pem".',
|
||||
organizationDetail: 'Organization details',
|
||||
fromWebsite: 'From website',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -493,7 +493,6 @@ const message = {
|
||||
home: {
|
||||
recommend: 'Recomendado',
|
||||
dir: 'Directorio',
|
||||
quickDirDelete: '¿Confirmar la eliminación de ({0})?',
|
||||
quickDir: 'Acceso rápido',
|
||||
minQuickJump: 'Agrega al menos una entrada de acceso rápido',
|
||||
maxQuickJump: 'Puedes agregar hasta cuatro entradas de acceso rápido',
|
||||
@@ -4302,7 +4301,8 @@ const message = {
|
||||
domainHelper: 'Un dominio por línea, admite * y dirección IP',
|
||||
pushDir: 'Guardar certificado en el directorio local',
|
||||
dir: 'Directorio',
|
||||
pushDirHelper: 'Se generarán los archivos "fullchain.pem" y "privkey.pem" en este directorio.',
|
||||
pushDirHelper:
|
||||
'Seleccione varios directorios o introduzca un directorio por línea. En cada directorio se generarán los archivos "fullchain.pem" y "privkey.pem".',
|
||||
organizationDetail: 'Detalles de la organización',
|
||||
fromWebsite: 'Desde el sitio web',
|
||||
dnsMauanlHelper: 'En modo manual, haz clic en solicitar tras la creación para obtener el valor DNS',
|
||||
|
||||
@@ -489,7 +489,6 @@ const message = {
|
||||
recommend: 'پیشنهادی',
|
||||
dir: 'دایرکتوری',
|
||||
alias: 'نام مستعار',
|
||||
quickDirDelete: 'حذف ({0}) را تأیید میکنید؟',
|
||||
quickDir: 'دسترسی سریع',
|
||||
minQuickJump: 'حداقل یک ورودی دسترسی سریع اضافه کنید',
|
||||
maxQuickJump: 'میتوانید حداکثر چهار ورودی دسترسی سریع اضافه کنید',
|
||||
@@ -4215,7 +4214,8 @@ const message = {
|
||||
domainHelper: 'هر خط یک نام دامنه، پشتیبانی از * و آدرس IP',
|
||||
pushDir: 'ارسال گواهی به دایرکتوری محلی',
|
||||
dir: 'دایرکتوری',
|
||||
pushDirHelper: 'فایل گواهی "fullchain.pem" و فایل کلید "privkey.pem" در این دایرکتوری تولید میشوند.',
|
||||
pushDirHelper:
|
||||
'چند دایرکتوری انتخاب کنید یا در هر خط یک دایرکتوری وارد کنید. فایلهای "fullchain.pem" و "privkey.pem" در هر دایرکتوری ایجاد میشوند.',
|
||||
organizationDetail: 'جزئیات سازمان',
|
||||
fromWebsite: 'از وبسایت',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -492,7 +492,6 @@ const message = {
|
||||
recommend: 'おすすめ',
|
||||
dir: 'ディレクトリ',
|
||||
alias: 'エイリアス',
|
||||
quickDirDelete: '({0})を削除しますか?',
|
||||
quickDir: 'クイックアクセス',
|
||||
minQuickJump: 'クイックアクセス項目を1つ以上追加してください',
|
||||
maxQuickJump: '最大4件のクイックアクセス項目を追加できます',
|
||||
@@ -4238,7 +4237,8 @@ const message = {
|
||||
domainHelper: '1行ごとに1つのドメイン名、 *およびIPアドレスをサポートします',
|
||||
pushDir: '証明書をローカルディレクトリにプッシュします',
|
||||
dir: 'ディレクトリ',
|
||||
pushDirHelper: '証明書ファイル「FullChain.PEM」とキーファイル「Privkey.Pem」がこのディレクトリで生成されます。',
|
||||
pushDirHelper:
|
||||
'複数のディレクトリを選択するか、1行に1つずつディレクトリを入力してください。各ディレクトリに「fullchain.pem」と「privkey.pem」が生成されます。',
|
||||
organizationDetail: '組織の詳細',
|
||||
fromWebsite: 'ウェブサイトから',
|
||||
dnsMauanlHelper: '手動解像度モードでは、作成後に適用ボタンをクリックしてDNS解像度値を取得する必要があります',
|
||||
|
||||
@@ -487,7 +487,6 @@ const message = {
|
||||
recommend: '추천',
|
||||
dir: '디렉토리',
|
||||
alias: '별칭',
|
||||
quickDirDelete: '({0})을(를) 삭제하시겠습니까?',
|
||||
quickDir: '빠른 액세스',
|
||||
minQuickJump: '최소 하나의 빠른 액세스 항목을 추가하세요',
|
||||
maxQuickJump: '빠른 액세스를 최대 4개까지 추가할 수 있습니다',
|
||||
@@ -4160,7 +4159,8 @@ const message = {
|
||||
domainHelper: '줄당 하나의 도메인 이름, * 및 IP 주소 지원',
|
||||
pushDir: '로컬 디렉토리로 인증서 푸시',
|
||||
dir: '디렉토리',
|
||||
pushDirHelper: '인증서 파일 "fullchain.pem" 및 키 파일 "privkey.pem"이 이 디렉토리에 생성됩니다.',
|
||||
pushDirHelper:
|
||||
'여러 디렉터리를 선택하거나 한 줄에 하나씩 입력하세요. 각 디렉터리에 "fullchain.pem"과 "privkey.pem" 파일이 생성됩니다.',
|
||||
organizationDetail: '조직 세부 정보',
|
||||
fromWebsite: '웹사이트에서 가져오기',
|
||||
dnsMauanlHelper: '수동 해석 모드에서는 생성 후 신청 버튼을 클릭하여 DNS 해석 값을 얻어야 합니다.',
|
||||
|
||||
@@ -487,7 +487,6 @@ const message = {
|
||||
recommend: 'ແນະນຳ',
|
||||
dir: 'ໄດເຣັກທໍຣີ',
|
||||
alias: 'ຊື່ຫຼິ້ນ',
|
||||
quickDirDelete: 'ຢືນຢັນການລຶບ ({0}) ຫຼືບໍ່?',
|
||||
quickDir: 'ເຂົ້າເຖິງດ່ວນ',
|
||||
minQuickJump: 'ເພີ່ມການເຂົ້າເຖິງດ່ວນຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
|
||||
maxQuickJump: 'ທ່ານສາມາດເພີ່ມການເຂົ້າເຖິງດ່ວນໄດ້ສູງສຸດສີ່ລາຍການ',
|
||||
@@ -4132,7 +4131,8 @@ const message = {
|
||||
domainHelper: 'ໜຶ່ງໂດເມນຕໍ່ແຖວ, ຮອງຮັບ * ແລະ ທີ່ຢູ່ IP',
|
||||
pushDir: 'ສົ່ງໃບຢັ້ງຢືນໄປຍັງໂຟນເດີໃນເຄື່ອງ',
|
||||
dir: 'ໂຟນເດີ',
|
||||
pushDirHelper: 'ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນໂຟນເດີນີ້.',
|
||||
pushDirHelper:
|
||||
'ເລືອກຫຼາຍໂຟນເດີ ຫຼື ປ້ອນໜຶ່ງໂຟນເດີຕໍ່ແຖວ. ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນແຕ່ລະໂຟນເດີ.',
|
||||
organizationDetail: 'ລາຍລະອຽດອົງກອນ',
|
||||
fromWebsite: 'ຈາກເວັບໄຊ',
|
||||
dnsMauanlHelper: 'ໃນໂໝດຈັດການ DNS ດ້ວຍຕົນເອງ, ທ່ານຕ້ອງຄລິກປຸ່ມ "ຂໍໃບຢັ້ງຢືນ" ຫຼັງຈາກສ້າງເພື່ອຮັບຄ່າ DNS',
|
||||
|
||||
@@ -499,7 +499,6 @@ const message = {
|
||||
recommend: 'Disyorkan',
|
||||
dir: 'Direktori',
|
||||
alias: 'Alias',
|
||||
quickDirDelete: 'Sahkan pemadaman ({0})?',
|
||||
quickDir: 'Akses Pantas',
|
||||
minQuickJump: 'Tambah sekurang-kurangnya satu entri akses pantas',
|
||||
maxQuickJump: 'Anda boleh menambah sehingga empat entri akses pantas',
|
||||
@@ -4316,7 +4315,8 @@ const message = {
|
||||
domainHelper: 'Satu nama domain setiap baris, menyokong * dan alamat IP',
|
||||
pushDir: 'Tolakkan sijil ke direktori tempatan',
|
||||
dir: 'Direktori',
|
||||
pushDirHelper: 'Fail sijil "fullchain.pem" dan fail kunci "privkey.pem" akan dihasilkan dalam direktori ini.',
|
||||
pushDirHelper:
|
||||
'Pilih beberapa direktori atau masukkan satu direktori pada setiap baris. Fail "fullchain.pem" dan "privkey.pem" akan dihasilkan dalam setiap direktori.',
|
||||
organizationDetail: 'Butiran organisasi',
|
||||
fromWebsite: 'Daripada laman web',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -492,7 +492,6 @@ const message = {
|
||||
recommend: 'Recomendado',
|
||||
dir: 'Diretório',
|
||||
alias: 'Apelido',
|
||||
quickDirDelete: 'Confirmar a exclusão de ({0})?',
|
||||
quickDir: 'Acesso rápido',
|
||||
minQuickJump: 'Adicione pelo menos uma entrada de acesso rápido',
|
||||
maxQuickJump: 'Você pode adicionar até quatro entradas de acesso rápido',
|
||||
@@ -4336,7 +4335,7 @@ const message = {
|
||||
pushDir: 'Enviar o certificado para o diretório local',
|
||||
dir: 'Diretório',
|
||||
pushDirHelper:
|
||||
'O arquivo do certificado "fullchain.pem" e o arquivo de chave "privkey.pem" serão gerados neste diretório.',
|
||||
'Selecione vários diretórios ou informe um diretório por linha. Os arquivos "fullchain.pem" e "privkey.pem" serão gerados em cada diretório.',
|
||||
organizationDetail: 'Detalhes da organização',
|
||||
fromWebsite: 'Do site',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -491,7 +491,6 @@ const message = {
|
||||
recommend: 'Рекомендуемое',
|
||||
dir: 'Каталог',
|
||||
alias: 'Псевдоним',
|
||||
quickDirDelete: 'Подтвердить удаление ({0})?',
|
||||
quickDir: 'Быстрый доступ',
|
||||
minQuickJump: 'Добавьте хотя бы одну запись быстрого доступа',
|
||||
maxQuickJump: 'Вы можете добавить до четырех записей быстрого доступа',
|
||||
@@ -4303,7 +4302,8 @@ const message = {
|
||||
domainHelper: 'Одно доменное имя в строке, поддерживает * и IP-адрес',
|
||||
pushDir: 'Отправить сертификат в локальную директорию',
|
||||
dir: 'Директория',
|
||||
pushDirHelper: 'В этой директории будут созданы файл сертификата "fullchain.pem" и файл ключа "privkey.pem".',
|
||||
pushDirHelper:
|
||||
'Выберите несколько директорий или укажите по одной директории в строке. В каждой директории будут созданы файлы "fullchain.pem" и "privkey.pem".',
|
||||
organizationDetail: 'Детали организации',
|
||||
fromWebsite: 'С веб-сайта',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -495,7 +495,6 @@ const message = {
|
||||
recommend: 'Önerilen',
|
||||
dir: 'Dizin',
|
||||
alias: 'Takma Ad',
|
||||
quickDirDelete: '({0}) silinsin mi?',
|
||||
quickDir: 'Hızlı Erişim',
|
||||
minQuickJump: 'En az bir hızlı erişim girdisi ekleyin',
|
||||
maxQuickJump: 'En fazla dört hızlı erişim girdisi ekleyebilirsiniz',
|
||||
@@ -4322,7 +4321,7 @@ const message = {
|
||||
pushDir: 'Sertifikayı yerel dizine aktar',
|
||||
dir: 'Dizin',
|
||||
pushDirHelper:
|
||||
'Sertifika dosyası "fullchain.pem" ve anahtar dosyası "privkey.pem" bu dizinde oluşturulacaktır.',
|
||||
'Birden fazla dizin seçin veya her satıra bir dizin girin. Her dizinde "fullchain.pem" ve "privkey.pem" dosyaları oluşturulacaktır.',
|
||||
organizationDetail: 'Organizasyon detayları',
|
||||
fromWebsite: 'Web sitesinden',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -473,7 +473,6 @@ const message = {
|
||||
recommend: '推薦',
|
||||
dir: '目錄',
|
||||
alias: '別名',
|
||||
quickDirDelete: '確定刪除({0})',
|
||||
quickDir: '快捷目錄',
|
||||
minQuickJump: '請至少設定一個快速跳轉入口',
|
||||
maxQuickJump: '最多可設定四個快速跳轉入口',
|
||||
@@ -3971,7 +3970,7 @@ const message = {
|
||||
domainHelper: '一行一個網域名稱,支援*和IP位址',
|
||||
pushDir: '推送憑證到本機目錄',
|
||||
dir: '目錄',
|
||||
pushDirHelper: '會在此目錄下產生兩個檔案,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem',
|
||||
pushDirHelper: '支援選擇多個目錄,也可每行填寫一個目錄。每個目錄下都會產生 fullchain.pem 和 privkey.pem。',
|
||||
organizationDetail: '機構詳情',
|
||||
fromWebsite: '從網站取得',
|
||||
dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值',
|
||||
|
||||
@@ -473,7 +473,6 @@ const message = {
|
||||
recommend: '推荐',
|
||||
dir: '目录',
|
||||
alias: '别名',
|
||||
quickDirDelete: '确定删除({0})',
|
||||
quickDir: '快捷目录',
|
||||
minQuickJump: '请至少设置一个快速跳转入口!',
|
||||
maxQuickJump: '最多可设置四个快速跳转入口!',
|
||||
@@ -4016,7 +4015,8 @@ const message = {
|
||||
domainHelper: '一行一个域名,支持*和IP地址',
|
||||
pushDir: '推送证书到本地目录',
|
||||
dir: '目录',
|
||||
pushDirHelper: '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem',
|
||||
pushDirHelper:
|
||||
'支持选择多个目录,也可每行填写一个目录。每个目录下都会生成证书文件 fullchain.pem 和密钥文件 privkey.pem',
|
||||
organizationDetail: '机构详情',
|
||||
fromWebsite: '从网站中获取',
|
||||
dnsMauanlHelper: '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值',
|
||||
|
||||
@@ -50,12 +50,20 @@
|
||||
<el-col :span="6" v-for="item in baseInfo.quickJump" :key="item.name">
|
||||
<span>{{ $t(item.title, 2) }}</span>
|
||||
<div class="count">
|
||||
<QuickDirectories
|
||||
v-if="item.name === 'File'"
|
||||
:count="item.detail"
|
||||
:disabled="!checkPermission('File')"
|
||||
@change="updateQuickDirectoryCount"
|
||||
/>
|
||||
<el-tooltip
|
||||
v-if="item.alias || item.detail.length > 20"
|
||||
:content="item.detail"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
:disabled="!checkPermission('File')"
|
||||
type="primary"
|
||||
@click="quickJump(item)"
|
||||
>
|
||||
{{ item.alias || item.detail.substring(0, 18) + '...' }}
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
link
|
||||
:disabled="!checkPermission(item.name)"
|
||||
@@ -442,7 +450,6 @@ import SystemStatus from '@/views/home/status/index.vue';
|
||||
import AppLauncher from '@/views/home/app/index.vue';
|
||||
import VCharts from '@/components/v-charts/index.vue';
|
||||
import QuickJump from '@/views/home/quick/index.vue';
|
||||
import QuickDirectories from '@/views/home/quick/directories.vue';
|
||||
import CardWithHeader from '@/components/card-with-header/index.vue';
|
||||
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
|
||||
import i18n from '@/lang';
|
||||
@@ -463,7 +470,7 @@ import {
|
||||
updateMemo,
|
||||
updateSetting,
|
||||
} from '@/api/modules/setting';
|
||||
import { routerToNameWithQuery, routerToPath } from '@/utils/router';
|
||||
import { routerToFileWithPath, routerToNameWithQuery, routerToPath } from '@/utils/router';
|
||||
import { getWelcomePage } from '@/api/modules/auth';
|
||||
import {
|
||||
clearDashboardCache,
|
||||
@@ -816,12 +823,10 @@ const onLoadBaseInfo = async (isInit: boolean, range: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateQuickDirectoryCount = (count: number) => {
|
||||
const item = baseInfo.value.quickJump.find((item) => item.name === 'File');
|
||||
if (item) item.detail = String(count);
|
||||
};
|
||||
|
||||
const quickJump = (item: any) => {
|
||||
if (item.name === 'File') {
|
||||
return routerToFileWithPath(item.detail);
|
||||
}
|
||||
return routerToPath(item.router);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
<template>
|
||||
<el-popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:width="680"
|
||||
:popper-style="{ maxWidth: 'calc(100vw - 32px)' }"
|
||||
:visible="visible"
|
||||
@update:visible="setVisible"
|
||||
:disabled="disabled"
|
||||
:title="$t('home.quickDir')"
|
||||
@show="load"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button link type="primary" :disabled="disabled">{{ count }}</el-button>
|
||||
</template>
|
||||
<div v-loading="loading" class="quick-directories">
|
||||
<div class="directory-search">
|
||||
<el-input
|
||||
v-model="searchInput"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
:placeholder="`${$t('file.path')} / ${$t('home.alias')}`"
|
||||
:aria-label="$t('commons.button.search')"
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-button :disabled="loading" @click="search">{{ $t('commons.button.confirm') }}</el-button>
|
||||
</div>
|
||||
<div class="directory-row directory-header">
|
||||
<span>{{ $t('file.path') }}</span>
|
||||
<span>{{ $t('home.alias') }}</span>
|
||||
<span />
|
||||
</div>
|
||||
<div class="directory-list">
|
||||
<div v-if="!filteredRows.length" class="directory-empty">{{ $t('commons.msg.noneData') }}</div>
|
||||
<div v-for="row in filteredRows" :key="row.key" class="directory-row">
|
||||
<el-input
|
||||
v-model="row.path"
|
||||
:readonly="!row.editing"
|
||||
:disabled="row.busy"
|
||||
:aria-label="$t('file.path')"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-button
|
||||
icon="Folder"
|
||||
:disabled="!row.editing || row.busy"
|
||||
:aria-label="$t('file.list')"
|
||||
@click="selectPath(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-model="row.alias"
|
||||
:readonly="!row.editing"
|
||||
:disabled="row.busy"
|
||||
:aria-label="$t('home.alias')"
|
||||
/>
|
||||
<div class="directory-actions">
|
||||
<el-button
|
||||
link
|
||||
:type="row.editing ? 'success' : 'primary'"
|
||||
:icon="row.editing ? 'Check' : 'Edit'"
|
||||
:aria-label="$t(row.editing ? 'commons.button.save' : 'commons.button.edit')"
|
||||
:disabled="loading || row.busy || (row.editing && !row.path.trim())"
|
||||
@click="row.editing ? save(row) : (row.editing = true)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="row.id || rows.length > 1"
|
||||
link
|
||||
:type="row.editing ? 'info' : 'danger'"
|
||||
:icon="row.editing ? 'Close' : 'Delete'"
|
||||
:aria-label="$t('commons.button.delete')"
|
||||
:disabled="loading || row.busy"
|
||||
@click="remove(row)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="row.id"
|
||||
link
|
||||
type="primary"
|
||||
icon="Position"
|
||||
:title="$t('firewall.quickJump')"
|
||||
:aria-label="$t('firewall.quickJump')"
|
||||
:disabled="loading || row.busy"
|
||||
@click="routerToFileWithPath(row.savedPath)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
class="directory-add"
|
||||
link
|
||||
type="primary"
|
||||
icon="Plus"
|
||||
:disabled="loading || !loaded"
|
||||
@click="add"
|
||||
>
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="directory-footer">
|
||||
<el-button @click="close">{{ $t('commons.button.close') }}</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
<FileList ref="fileRef" @choose="loadPath" @close="selectingRow = undefined" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { routerToFileWithPath } from '@/utils/router';
|
||||
import FileList from '@/components/file-list/index.vue';
|
||||
import { Dashboard } from '@/api/interface/dashboard';
|
||||
import {
|
||||
loadQuickDirectories,
|
||||
createQuickDirectory,
|
||||
updateQuickDirectory,
|
||||
deleteQuickDirectory,
|
||||
} from '@/api/modules/dashboard';
|
||||
|
||||
defineProps<{ count: string; disabled: boolean }>();
|
||||
const emit = defineEmits<{ (e: 'change', count: number): void }>();
|
||||
type DirectoryRow = Dashboard.QuickDirectory & {
|
||||
key: number;
|
||||
savedPath: string;
|
||||
editing: boolean;
|
||||
busy: boolean;
|
||||
};
|
||||
const rows = ref<DirectoryRow[]>([]);
|
||||
const searchInput = ref('');
|
||||
const searchTerm = ref('');
|
||||
const filteredRows = computed(() =>
|
||||
rows.value.filter(
|
||||
(row) =>
|
||||
!row.id ||
|
||||
row.editing ||
|
||||
row.path.toLowerCase().includes(searchTerm.value) ||
|
||||
(row.alias ?? '').toLowerCase().includes(searchTerm.value),
|
||||
),
|
||||
);
|
||||
const search = () => {
|
||||
searchTerm.value = searchInput.value.trim().toLowerCase();
|
||||
};
|
||||
const fileRef = ref<InstanceType<typeof FileList>>();
|
||||
const selectingRow = ref<DirectoryRow>();
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const loaded = ref(false);
|
||||
let nextKey = 0;
|
||||
|
||||
const setVisible = (value: boolean) => {
|
||||
if (value) visible.value = true;
|
||||
};
|
||||
const close = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const selectPath = (row: DirectoryRow) => {
|
||||
if (!row.editing || row.busy) return;
|
||||
selectingRow.value = row;
|
||||
fileRef.value?.acceptParams({ path: row.path, dir: false, isAll: true });
|
||||
};
|
||||
const loadPath = (path: string) => {
|
||||
const row = selectingRow.value;
|
||||
if (row?.editing && !row.busy) row.path = path;
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
rows.value.push({
|
||||
key: nextKey++,
|
||||
id: 0,
|
||||
path: '',
|
||||
alias: '',
|
||||
savedPath: '',
|
||||
editing: true,
|
||||
busy: false,
|
||||
});
|
||||
};
|
||||
const updateCount = () => emit('change', rows.value.filter((row) => row.id).length);
|
||||
const load = async () => {
|
||||
if (loading.value || rows.value.some((row) => row.busy)) return;
|
||||
loading.value = true;
|
||||
loaded.value = false;
|
||||
searchInput.value = '';
|
||||
searchTerm.value = '';
|
||||
rows.value = [];
|
||||
try {
|
||||
const { data } = await loadQuickDirectories();
|
||||
rows.value = data.map((item) => ({
|
||||
...item,
|
||||
alias: item.alias ?? '',
|
||||
key: nextKey++,
|
||||
savedPath: item.path,
|
||||
editing: false,
|
||||
busy: false,
|
||||
}));
|
||||
if (!rows.value.length) add();
|
||||
loaded.value = true;
|
||||
updateCount();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const save = async (row: DirectoryRow) => {
|
||||
if (row.busy || !row.path.trim()) return;
|
||||
row.busy = true;
|
||||
try {
|
||||
const payload = { path: row.path, alias: (row.alias ?? '').trim() };
|
||||
const { data } = row.id
|
||||
? await updateQuickDirectory({ id: row.id, legacy: row.legacy ?? false, ...payload })
|
||||
: await createQuickDirectory(payload);
|
||||
Object.assign(row, data, { savedPath: data.path, editing: false });
|
||||
updateCount();
|
||||
} finally {
|
||||
row.busy = false;
|
||||
}
|
||||
};
|
||||
const remove = async (row: DirectoryRow) => {
|
||||
if (row.busy) return;
|
||||
if (row.id) {
|
||||
row.busy = true;
|
||||
try {
|
||||
await ElMessageBox.confirm('', i18n.global.t('home.quickDirDelete', [row.savedPath]), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
customClass: 'quick-directory-delete',
|
||||
type: 'warning',
|
||||
center: true,
|
||||
});
|
||||
} catch {
|
||||
row.busy = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteQuickDirectory(row.id, row.legacy ?? false);
|
||||
} catch (error) {
|
||||
row.busy = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
rows.value = rows.value.filter((item) => item.key !== row.key);
|
||||
if (!rows.value.length) add();
|
||||
updateCount();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quick-directories {
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.directory-search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
.directory-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.directory-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 96px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
.directory-header {
|
||||
font-weight: 600;
|
||||
}
|
||||
.directory-list {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.directory-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.directory-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.directory-add {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.quick-directory-delete {
|
||||
.el-message-box__title {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: #59472a;
|
||||
color: #f5cf87;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.el-message-box__status {
|
||||
flex-shrink: 0;
|
||||
font-size: 24px;
|
||||
color: #f2b329;
|
||||
}
|
||||
.el-message-box__content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,25 @@
|
||||
<el-table-column prop="isShow" :label="$t('setting.ifShow')">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.isShow" />
|
||||
<div v-if="row.name === 'File' && row.isShow">
|
||||
<el-input v-model="row.detail" class="w-full">
|
||||
<template #prepend>
|
||||
<el-button
|
||||
class="w-16"
|
||||
v-if="row.name === 'File' && row.isShow"
|
||||
icon="Folder"
|
||||
@click="fileRef.acceptParams({ path: row.detail, isAll: true })"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model="row.alias" class="mt-1">
|
||||
<template #prepend>
|
||||
<el-button class="w-16">
|
||||
{{ $t('home.alias') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ComplexTable>
|
||||
@@ -20,12 +39,14 @@
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<FileList ref="fileRef" @choose="loadDir" />
|
||||
</DrawerPro>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgInfo, MsgSuccess } from '@/utils/message';
|
||||
import FileList from '@/components/file-list/index.vue';
|
||||
import { changeQuick, loadQuickOption } from '@/api/modules/dashboard';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
@@ -36,6 +57,7 @@ const acceptParams = (): void => {
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const quickOptions = ref([]);
|
||||
const fileRef = ref();
|
||||
|
||||
const search = async () => {
|
||||
loading.value = true;
|
||||
@@ -64,17 +86,26 @@ const onChangeShow = async () => {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
await changeQuick(quickOptions.value.map(({ id, isShow }) => ({ id, isShow })))
|
||||
await changeQuick(quickOptions.value)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
drawerVisible.value = false;
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const loadDir = async (path: string) => {
|
||||
for (const item of quickOptions.value) {
|
||||
if (item.name === 'File') {
|
||||
item.detail = path;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
|
||||
@@ -41,14 +41,21 @@
|
||||
<el-checkbox v-model="obtain.pushDir" :label="$t('ssl.pushDir')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="obtain.pushDir">
|
||||
<el-input v-model.trim="obtain.dir">
|
||||
<template #prepend>
|
||||
<el-button
|
||||
icon="Folder"
|
||||
@click="fileRef.acceptParams({ path: obtain.dir, dir: true })"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
icon="Folder"
|
||||
class="mb-2"
|
||||
@click="
|
||||
fileRef.acceptParams({
|
||||
path: obtain.dir.split('\n')[0],
|
||||
dir: true,
|
||||
multiple: true,
|
||||
selectedPaths: obtain.dir.split('\n').filter(Boolean),
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t('ssl.dir') }}
|
||||
</el-button>
|
||||
<el-input v-model.trim="obtain.dir" type="textarea" :rows="3" />
|
||||
<span class="input-help">
|
||||
{{ $t('ssl.pushDirHelper') }}
|
||||
</span>
|
||||
@@ -157,8 +164,8 @@ const resetForm = () => {
|
||||
obtain.value = initData();
|
||||
};
|
||||
|
||||
const getPath = (dir: string) => {
|
||||
obtain.value.dir = dir;
|
||||
const getPath = (dirs: string[]) => {
|
||||
obtain.value.dir = dirs.join('\n');
|
||||
};
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
@@ -127,11 +127,21 @@
|
||||
<el-checkbox v-model="ssl.pushDir" :label="$t('ssl.pushDir')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="ssl.pushDir">
|
||||
<el-input v-model.trim="ssl.dir">
|
||||
<template #prepend>
|
||||
<el-button icon="Folder" @click="fileRef.acceptParams({ path: ssl.dir, dir: true })" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
icon="Folder"
|
||||
class="mb-2"
|
||||
@click="
|
||||
fileRef.acceptParams({
|
||||
path: ssl.dir.split('\n')[0],
|
||||
dir: true,
|
||||
multiple: true,
|
||||
selectedPaths: ssl.dir.split('\n').filter(Boolean),
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t('ssl.dir') }}
|
||||
</el-button>
|
||||
<el-input v-model.trim="ssl.dir" type="textarea" :rows="3" />
|
||||
<span class="input-help">
|
||||
{{ $t('ssl.pushDirHelper') }}
|
||||
</span>
|
||||
@@ -347,8 +357,8 @@ const acceptParams = (op: string, websiteSSL?: Website.SSLDTO) => {
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const getPath = (dir: string) => {
|
||||
ssl.value.dir = dir;
|
||||
const getPath = (dirs: string[]) => {
|
||||
ssl.value.dir = dirs.join('\n');
|
||||
};
|
||||
|
||||
const getAcmeAccounts = async () => {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{{ ssl.acmeAccount.email }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('ssl.pushDir')" v-if="ssl.pushDir">
|
||||
{{ ssl.dir }}
|
||||
<div v-for="dir in ssl.dir.split('\n')" :key="dir">{{ dir }}</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user