Compare commits

..
Author SHA1 Message Date
guoyuqi 519a19d507 feat: ssl push multiple directories 2026-10-20 16:27:24 +08:00
33 changed files with 220 additions and 685 deletions
-84
View File
@@ -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)
}
+1 -21
View File
@@ -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 {
-7
View File
@@ -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 -69
View File
@@ -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
}
+6 -16
View File
@@ -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":
-49
View File
@@ -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)
}
+12 -3
View File
@@ -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 {
+21 -8
View File
@@ -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 != "" {
+33 -18
View File
@@ -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}))
}
}
-1
View File
@@ -115,7 +115,6 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics,
migrations.AddVLLMSampleInterval,
migrations.AddQuickDirectory,
}
}
-7
View File
@@ -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{})
},
}
-4
View File
@@ -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)
-6
View File
@@ -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;
+1 -9
View File
@@ -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 });
+38 -12
View File
@@ -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);
+1 -2
View File
@@ -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:
+2 -2
View File
@@ -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',
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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解像度値を取得する必要があります',
+2 -2
View File
@@ -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 해석 값을 얻어야 합니다.',
+2 -2
View File
@@ -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',
+2 -2
View File
@@ -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:
+1 -2
View File
@@ -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:
+2 -2
View File
@@ -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:
+1 -2
View File
@@ -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:
+1 -2
View File
@@ -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 解析值',
+2 -2
View File
@@ -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 解析值',
+18 -13
View File
@@ -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>
+32 -1
View File
@@ -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>