2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 03:07:20 +08:00

feat: 添加项目默认目录配置及目录跳转功能 (#1233)

* Initial plan

* feat: 添加项目默认目录配置及目录跳转功能

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: 为项目默认目录添加回退默认值

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-13 22:25:20 +08:00
committed by GitHub
parent 1f55c2448d
commit 908509e06b
10 changed files with 72 additions and 15 deletions

View File

@@ -85,7 +85,7 @@ func initWeb() (*app.Web, error) {
taskService := service.NewTaskService(taskRepo)
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
projectRepo := data.NewProjectRepo(locale, db, logger)
projectService := service.NewProjectService(projectRepo)
projectService := service.NewProjectService(projectRepo, settingRepo)
databaseService := service.NewDatabaseService(databaseRepo)
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)

View File

@@ -17,6 +17,7 @@ const (
SettingKeyMonitorDays SettingKey = "monitor_days"
SettingKeyBackupPath SettingKey = "backup_path"
SettingKeyWebsitePath SettingKey = "website_path"
SettingKeyProjectPath SettingKey = "project_path"
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
SettingKeyWebsiteCipherSuites SettingKey = "website_tls_cipher_suites"
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"

View File

@@ -225,6 +225,10 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
if err != nil {
return nil, err
}
projectPath, err := r.Get(biz.SettingKeyProjectPath)
if err != nil {
return nil, err
}
hiddenMenu, err := r.GetSlice(biz.SettingHiddenMenu)
if err != nil {
return nil, err
@@ -261,6 +265,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
BindUA: r.conf.HTTP.BindUA,
WebsitePath: websitePath,
BackupPath: backupPath,
ProjectPath: projectPath,
HiddenMenu: hiddenMenu,
CustomLogo: customLogo,
Port: r.conf.HTTP.Port,
@@ -291,6 +296,9 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyProjectPath, req.ProjectPath); err != nil {
return false, err
}
if err := r.SetSlice(biz.SettingHiddenMenu, req.HiddenMenu); err != nil {
return false, err
}

View File

@@ -6,7 +6,7 @@ type ProjectCreate struct {
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Type types.ProjectType `form:"type" json:"type" validate:"required|in:general,php,java,go,python,nodejs"`
Description string `form:"description" json:"description"`
RootDir string `form:"root_dir" json:"root_dir" validate:"required"`
RootDir string `form:"root_dir" json:"root_dir"`
WorkingDir string `form:"working_dir" json:"working_dir"`
ExecStart string `form:"exec_start" json:"exec_start"`
User string `form:"user" json:"user"`

View File

@@ -19,6 +19,7 @@ type SettingPanel struct {
BindUA []string `json:"bind_ua"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
ProjectPath string `json:"project_path" validate:"required"`
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL
Port uint `json:"port" validate:"required|min:1|max:65535"`

View File

@@ -2,6 +2,7 @@ package service
import (
"net/http"
"path/filepath"
"github.com/libtnb/chix"
@@ -12,11 +13,13 @@ import (
type ProjectService struct {
projectRepo biz.ProjectRepo
settingRepo biz.SettingRepo
}
func NewProjectService(project biz.ProjectRepo) *ProjectService {
func NewProjectService(project biz.ProjectRepo, setting biz.SettingRepo) *ProjectService {
return &ProjectService{
projectRepo: project,
settingRepo: setting,
}
}
@@ -63,6 +66,11 @@ func (s *ProjectService) Create(w http.ResponseWriter, r *http.Request) {
return
}
if len(req.RootDir) == 0 {
req.RootDir, _ = s.settingRepo.Get(biz.SettingKeyProjectPath, "/opt/ace/projects")
req.RootDir = filepath.Join(req.RootDir, req.Name)
}
project, err := s.projectRepo.Create(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)

View File

@@ -25,7 +25,7 @@ const phpFrameworks = [
const createModel = ref({
name: '',
type: '',
root_dir: '/opt/ace/projects',
root_dir: '',
working_dir: '',
exec_start: '',
user: 'www'
@@ -98,7 +98,7 @@ const handleCreate = async () => {
createModel.value = {
name: '',
type: '',
root_dir: '/opt/ace/projects',
root_dir: '',
working_dir: '',
exec_start: '',
user: 'www'
@@ -141,13 +141,17 @@ const modalTitle = computed(() => {
/>
</n-form-item>
<n-form-item path="root_dir" :label="$gettext('Project Directory')" required>
<n-form-item path="root_dir" :label="$gettext('Project Directory')">
<n-input-group>
<n-input
v-model:value="createModel.root_dir"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Project root directory')"
:placeholder="
$gettext(
'Project root directory (if left empty, defaults to project directory/project name)'
)
"
/>
<n-button @click="handleSelectPath">
<template #icon>

View File

@@ -5,6 +5,7 @@ import { useGettext } from 'vue3-gettext'
import project from '@/api/panel/project'
import systemctl from '@/api/panel/systemctl'
import RealtimeLog from '@/components/common/RealtimeLog.vue'
import { useFileStore } from '@/store'
const type = defineModel<string>('type', { type: String, required: true })
const createModal = defineModel<boolean>('createModal', { type: Boolean, required: true })
@@ -13,7 +14,9 @@ const editId = defineModel<number>('editId', { type: Number, required: true })
const logModal = ref(false)
const logService = ref('')
const fileStore = useFileStore()
const { $gettext } = useGettext()
const router = useRouter()
const selectedRowKeys = ref<any>([])
const typeMap: Record<string, string> = {
@@ -66,7 +69,20 @@ const columns: any = [
key: 'root_dir',
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true }
render(row: any) {
return h(
NTag,
{
class: 'cursor-pointer hover:opacity-60',
type: 'info',
onClick: () => {
fileStore.path = row.root_dir
router.push({ name: 'file-index' })
}
},
{ default: () => row.root_dir }
)
}
},
{
title: $gettext('Actions'),

View File

@@ -37,6 +37,7 @@ const { data: model } = useRequest(setting.list, {
bind_ua: [],
website_path: '',
backup_path: '',
project_path: '',
hidden_menu: [],
custom_logo: '',
https: false,

View File

@@ -16,14 +16,17 @@ const model = defineModel<any>('model', { type: Object, required: true })
// 目录选择器
const showPathSelector = ref(false)
const pathSelectorPath = ref('/opt/ace')
const pathSelectorTarget = ref<'website' | 'backup'>('website')
const pathSelectorTarget = ref<'website' | 'backup' | 'project'>('website')
const handleSelectPath = (target: 'website' | 'backup') => {
const handleSelectPath = (target: 'website' | 'backup' | 'project') => {
pathSelectorTarget.value = target
pathSelectorPath.value =
target === 'website'
? model.value.website_path || '/opt/ace/sites'
: model.value.backup_path || '/opt/ace/backup'
if (target === 'website') {
pathSelectorPath.value = model.value.website_path || '/opt/ace/sites'
} else if (target === 'backup') {
pathSelectorPath.value = model.value.backup_path || '/opt/ace/backup'
} else {
pathSelectorPath.value = model.value.project_path || '/opt/ace/projects'
}
showPathSelector.value = true
}
@@ -31,8 +34,10 @@ watch(showPathSelector, (val) => {
if (!val && pathSelectorPath.value) {
if (pathSelectorTarget.value === 'website') {
model.value.website_path = pathSelectorPath.value
} else {
} else if (pathSelectorTarget.value === 'backup') {
model.value.backup_path = pathSelectorPath.value
} else {
model.value.project_path = pathSelectorPath.value
}
}
})
@@ -138,6 +143,19 @@ const menus = computed<TreeSelectOption[]>(() => {
</n-button>
</n-input-group>
</n-form-item>
<n-form-item :label="$gettext('Default Project Directory')">
<n-input-group>
<n-input
v-model:value="model.project_path"
:placeholder="$gettext('/opt/ace/projects')"
/>
<n-button @click="handleSelectPath('project')">
<template #icon>
<i-mdi-folder-open />
</template>
</n-button>
</n-input-group>
</n-form-item>
<n-form-item :label="$gettext('Custom Logo')">
<n-input
v-model:value="model.custom_logo"